diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -162,6 +162,19 @@ def admin_routes(config): name='edit_user_audit_logs', pattern='/users/{user_id:\d+}/edit/audit') + # user groups admin + config.add_route( + name='user_groups', + pattern='/user_groups') + + config.add_route( + name='user_groups_data', + pattern='/user_groups_data') + + config.add_route( + name='user_group_members_data', + pattern='/user_groups/{user_group_id:\d+}/members') + def includeme(config): settings = config.get_settings() diff --git a/rhodecode/apps/admin/tests/test_admin_user_groups.py b/rhodecode/apps/admin/tests/test_admin_user_groups.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_user_groups.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# 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 . +# +# 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/ + +import pytest + +from rhodecode.model.db import UserGroup, User +from rhodecode.model.meta import Session + +from rhodecode.tests import ( + TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'user_groups': ADMIN_PREFIX + '/user_groups', + 'user_groups_data': ADMIN_PREFIX + '/user_groups_data', + 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestAdminUserGroupsView(TestController): + + def test_show_users(self): + self.log_user() + self.app.get(route_path('user_groups')) + + def test_show_user_groups_data(self, xhr_header): + self.log_user() + response = self.app.get(route_path( + 'user_groups_data'), extra_environ=xhr_header) + + all_user_groups = UserGroup.query().count() + assert response.json['recordsTotal'] == all_user_groups + + def test_show_user_groups_data_filtered(self, xhr_header): + self.log_user() + response = self.app.get(route_path( + 'user_groups_data', params={'search[value]': 'empty_search'}), + extra_environ=xhr_header) + + all_user_groups = UserGroup.query().count() + assert response.json['recordsTotal'] == all_user_groups + assert response.json['recordsFiltered'] == 0 + + def test_usergroup_escape(self, user_util, xhr_header): + self.log_user() + + xss_img = '' + user = user_util.create_user() + user.name = xss_img + user.lastname = xss_img + Session().add(user) + Session().commit() + + user_group = user_util.create_user_group() + + user_group.users_group_name = xss_img + user_group.user_group_description = 'DESC' + + response = self.app.get( + route_path('user_groups_data'), extra_environ=xhr_header) + + response.mustcontain( + '<strong onload="alert();">DESC</strong>') + response.mustcontain( + '<img src="/image1" onload="' + 'alert('Hello, World!');">') + + def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util): + self.log_user() + ug = user_util.create_user_group() + response = self.app.get( + route_path('user_group_members_data', user_group_id=ug.users_group_id), + extra_environ=xhr_header) + + assert response.json == {'members': []} + + def test_edit_user_group_autocomplete_members(self, xhr_header, user_util): + self.log_user() + members = [u.user_id for u in User.get_all()] + ug = user_util.create_user_group(members=members) + response = self.app.get( + route_path('user_group_members_data', user_group_id=ug.users_group_id), + extra_environ=xhr_header) + + assert len(response.json['members']) == len(members) diff --git a/rhodecode/apps/admin/views/user_groups.py b/rhodecode/apps/admin/views/user_groups.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/user_groups.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# 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 . +# +# 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/ + +import logging +import datetime + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from rhodecode.lib.helpers import Page +from rhodecode.model.scm import UserGroupList +from rhodecode_tools.lib.ext_json import json + +from rhodecode.apps._base import BaseAppView, DataGridAppView +from rhodecode.lib.auth import ( + LoginRequired, HasPermissionAllDecorator, CSRFRequired, NotAnonymous, + HasUserGroupPermissionAnyDecorator) +from rhodecode.lib import helpers as h +from rhodecode.lib.utils import PartialRenderer +from rhodecode.lib.utils2 import safe_int, safe_unicode +from rhodecode.model.auth_token import AuthTokenModel +from rhodecode.model.user import UserModel +from rhodecode.model.user_group import UserGroupModel +from rhodecode.model.db import User, UserGroup, UserGroupMember, or_, count +from rhodecode.model.meta import Session + +log = logging.getLogger(__name__) + + +class AdminUserGroupsView(BaseAppView, DataGridAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + # permission check in data loading of + # `user_groups_list_data` via UserGroupList + @NotAnonymous() + @view_config( + route_name='user_groups', request_method='GET', + renderer='rhodecode:templates/admin/user_groups/user_groups.mako') + def user_groups_list(self): + c = self.load_default_context() + return self._get_template_context(c) + + # permission check inside + @NotAnonymous() + @view_config( + route_name='user_groups_data', request_method='GET', + renderer='json_ext', xhr=True) + def user_groups_list_data(self): + column_map = { + 'active': 'users_group_active', + 'description': 'user_group_description', + 'members': 'members_total', + 'owner': 'user_username', + 'sync': 'group_data' + } + draw, start, limit = self._extract_chunk(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) + + _render = PartialRenderer('data_table/_dt_elements.mako') + + def user_group_name(user_group_id, user_group_name): + return _render("user_group_name", user_group_id, user_group_name) + + def user_group_actions(user_group_id, user_group_name): + return _render("user_group_actions", user_group_id, user_group_name) + + def user_profile(username): + return _render('user_profile', username) + + user_groups_data_total_count = UserGroup.query().count() + + member_count = count(UserGroupMember.user_id) + base_q = Session.query( + UserGroup.users_group_name, + UserGroup.user_group_description, + UserGroup.users_group_active, + UserGroup.users_group_id, + UserGroup.group_data, + User, + member_count.label('member_count') + ) \ + .outerjoin(UserGroupMember) \ + .join(User, User.user_id == UserGroup.user_id) \ + .group_by(UserGroup, User) + + if search_q: + like_expression = u'%{}%'.format(safe_unicode(search_q)) + base_q = base_q.filter(or_( + UserGroup.users_group_name.ilike(like_expression), + )) + + user_groups_data_total_filtered_count = base_q.count() + + if order_by == 'members_total': + sort_col = member_count + elif order_by == 'user_username': + sort_col = User.username + else: + sort_col = getattr(UserGroup, order_by, None) + + if isinstance(sort_col, count) or sort_col: + if order_dir == 'asc': + sort_col = sort_col.asc() + else: + sort_col = sort_col.desc() + + base_q = base_q.order_by(sort_col) + base_q = base_q.offset(start).limit(limit) + + # authenticated access to user groups + user_group_list = base_q.all() + auth_user_group_list = UserGroupList( + user_group_list, perm_set=['usergroup.admin']) + + user_groups_data = [] + for user_gr in auth_user_group_list: + user_groups_data.append({ + "users_group_name": user_group_name( + user_gr.users_group_id, h.escape(user_gr.users_group_name)), + "name_raw": h.escape(user_gr.users_group_name), + "description": h.escape(user_gr.user_group_description), + "members": user_gr.member_count, + # NOTE(marcink): because of advanced query we + # need to load it like that + "sync": UserGroup._load_group_data( + user_gr.group_data).get('extern_type'), + "active": h.bool2icon(user_gr.users_group_active), + "owner": user_profile(user_gr.User.username), + "action": user_group_actions( + user_gr.users_group_id, user_gr.users_group_name) + }) + + data = ({ + 'draw': draw, + 'data': user_groups_data, + 'recordsTotal': user_groups_data_total_count, + 'recordsFiltered': user_groups_data_total_filtered_count, + }) + + return data + + @LoginRequired() + @HasUserGroupPermissionAnyDecorator('usergroup.admin') + @view_config( + route_name='user_group_members_data', request_method='GET', + renderer='json_ext', xhr=True) + def user_group_members(self): + """ + Return members of given user group + """ + user_group_id = self.request.matchdict['user_group_id'] + user_group = UserGroup.get_or_404(user_group_id) + group_members_obj = sorted((x.user for x in user_group.members), + key=lambda u: u.username.lower()) + + group_members = [ + { + 'id': user.user_id, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'username': user.username, + 'icon_link': h.gravatar_url(user.email, 30), + 'value_display': h.person(user.email), + 'value': user.username, + 'value_type': 'user', + 'active': user.active, + } + for user in group_members_obj + ] + + return { + 'members': group_members + } diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -78,8 +78,13 @@ class AdminUsersView(BaseAppView, DataGr route_name='users_data', request_method='GET', renderer='json_ext', xhr=True) def users_list_data(self): + column_map = { + 'first_name': 'name', + 'last_name': 'lastname', + } draw, start, limit = self._extract_chunk(self.request) - search_q, order_by, order_dir = self._extract_ordering(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) _render = self.request.get_partial_renderer( 'data_table/_dt_elements.mako') diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -279,8 +279,6 @@ def make_map(config): controller='admin/user_groups') as m: m.connect('users_groups', '/user_groups', action='create', conditions={'method': ['POST']}) - m.connect('users_groups', '/user_groups', - action='index', conditions={'method': ['GET']}) m.connect('new_users_group', '/user_groups/new', action='new', conditions={'method': ['GET']}) m.connect('update_users_group', '/user_groups/{user_group_id}', @@ -317,10 +315,6 @@ def make_map(config): '/user_groups/{user_group_id}/edit/advanced/sync', action='edit_advanced_set_synchronization', conditions={'method': ['POST']}) - m.connect('edit_user_group_members', - '/user_groups/{user_group_id}/edit/members', jsroute=True, - action='user_group_members', conditions={'method': ['GET']}) - # ADMIN DEFAULTS REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/defaults') as m: diff --git a/rhodecode/controllers/admin/user_groups.py b/rhodecode/controllers/admin/user_groups.py --- a/rhodecode/controllers/admin/user_groups.py +++ b/rhodecode/controllers/admin/user_groups.py @@ -103,42 +103,6 @@ class UserGroupsController(BaseControlle return True return False - # permission check inside - @NotAnonymous() - def index(self): - # TODO(marcink): remove bind to self.request after pyramid migration - self.request = c.pyramid_request - _render = self.request.get_partial_renderer( - 'data_table/_dt_elements.mako') - - def user_group_name(user_group_id, user_group_name): - return _render("user_group_name", user_group_id, user_group_name) - - def user_group_actions(user_group_id, user_group_name): - return _render("user_group_actions", user_group_id, user_group_name) - - # json generate - group_iter = UserGroupList(UserGroup.query().all(), - perm_set=['usergroup.admin']) - - user_groups_data = [] - for user_gr in group_iter: - user_groups_data.append({ - "group_name": user_group_name( - user_gr.users_group_id, h.escape(user_gr.users_group_name)), - "group_name_raw": user_gr.users_group_name, - "desc": h.escape(user_gr.user_group_description), - "members": len(user_gr.members), - "sync": user_gr.group_data.get('extern_type'), - "active": h.bool2icon(user_gr.users_group_active), - "owner": h.escape(h.link_to_user(user_gr.user.username)), - "action": user_group_actions( - user_gr.users_group_id, user_gr.users_group_name) - }) - - c.data = json.dumps(user_groups_data) - return render('admin/user_groups/user_groups.mako') - @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') @auth.CSRFRequired() def create(self): @@ -482,33 +446,3 @@ class UserGroupsController(BaseControlle return redirect( url('edit_user_group_advanced', user_group_id=user_group_id)) - @HasUserGroupPermissionAnyDecorator('usergroup.admin') - @XHRRequired() - @jsonify - def user_group_members(self, user_group_id): - """ - Return members of given user group - """ - user_group_id = safe_int(user_group_id) - user_group = UserGroup.get_or_404(user_group_id) - group_members_obj = sorted((x.user for x in user_group.members), - key=lambda u: u.username.lower()) - - group_members = [ - { - 'id': user.user_id, - 'first_name': user.first_name, - 'last_name': user.last_name, - 'username': user.username, - 'icon_link': h.gravatar_url(user.email, 30), - 'value_display': h.person(user.email), - 'value': user.username, - 'value_type': 'user', - 'active': user.active, - } - for user in group_members_obj - ] - - return { - 'members': group_members - } diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -132,9 +132,9 @@ def get_user_group_slug(request): if _group: _group = _group.users_group_name except Exception: - log.debug(traceback.format_exc()) + log.exception('Failed to get user group by id') # catch all failures here - pass + return None return _group diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -41,6 +41,7 @@ from sqlalchemy.ext.hybrid import hybrid from sqlalchemy.orm import ( relationship, joinedload, class_mapper, validates, aliased) from sqlalchemy.sql.expression import true +from sqlalchemy.sql.functions import coalesce, count # noqa from beaker.cache import cache_region from zope.cachedescriptors.property import Lazy as LazyProperty @@ -1205,7 +1206,17 @@ class UserGroup(Base, BaseModel): user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all') user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all') - user = relationship('User') + user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id") + + @classmethod + def _load_group_data(cls, column): + if not column: + return {} + + try: + return json.loads(column) or {} + except TypeError: + return {} @hybrid_property def description_safe(self): @@ -1214,13 +1225,11 @@ class UserGroup(Base, BaseModel): @hybrid_property def group_data(self): - if not self._group_data: - return {} - - try: - return json.loads(self._group_data) - except TypeError: - return {} + return self._load_group_data(self._group_data) + + @group_data.expression + def group_data(self, **kwargs): + return self._group_data @group_data.setter def group_data(self, val): diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -14,7 +14,6 @@ function registerRCRoutes() { // routes registration pyroutes.register('new_repo', '/_admin/create_repository', []); pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']); - pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); pyroutes.register('favicon', '/favicon.ico', []); pyroutes.register('robots', '/robots.txt', []); pyroutes.register('auth_home', '/_admin/auth*traverse', []); @@ -72,6 +71,9 @@ function registerRCRoutes() { pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']); pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']); pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']); + pyroutes.register('user_groups', '/_admin/user_groups', []); + pyroutes.register('user_groups_data', '/_admin/user_groups_data', []); + pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']); pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []); pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []); pyroutes.register('channelstream_proxy', '/_channelstream', []); diff --git a/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako b/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako --- a/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako +++ b/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako @@ -166,7 +166,7 @@ if (suggestion.value_type == 'user_group') { $.getJSON( - pyroutes.url('edit_user_group_members', + pyroutes.url('user_group_members_data', {'user_group_id': suggestion.id}), function(data) { $.each(data.members, function(idx, user) { diff --git a/rhodecode/templates/admin/user_groups/user_groups.mako b/rhodecode/templates/admin/user_groups/user_groups.mako --- a/rhodecode/templates/admin/user_groups/user_groups.mako +++ b/rhodecode/templates/admin/user_groups/user_groups.mako @@ -38,26 +38,31 @@