diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -30,6 +30,7 @@ from rhodecode.lib.vcs.exceptions import from rhodecode.model import repo from rhodecode.model import repo_group from rhodecode.model import user_group +from rhodecode.model import user from rhodecode.model.db import User from rhodecode.model.scm import ScmModel @@ -267,6 +268,20 @@ class UserGroupAppView(BaseAppView): self.db_user_group_name = self.db_user_group.users_group_name +class UserAppView(BaseAppView): + def __init__(self, context, request): + super(UserAppView, self).__init__(context, request) + self.db_user = request.db_user + self.db_user_id = self.db_user.user_id + + _ = self.request.translate + if not request.db_user_supports_default: + if self.db_user.username == User.DEFAULT_USER: + h.flash(_("Editing user `{}` is disabled.".format( + User.DEFAULT_USER)), category='warning') + raise HTTPFound(h.route_path('users')) + + class DataGridAppView(object): """ Common class to have re-usable grid rendering components @@ -483,17 +498,63 @@ class UserGroupRoutePredicate(object): user_group_id = info['match']['user_group_id'] user_group_model = user_group.UserGroup() - by_name_match = user_group_model.get( + by_id_match = user_group_model.get( user_group_id, cache=True) - if by_name_match: + if by_id_match: # register this as request object we can re-use later - request.db_user_group = by_name_match + request.db_user_group = by_id_match return True return False +class UserRoutePredicateBase(object): + supports_default = None + + def __init__(self, val, config): + self.val = val + + def text(self): + raise NotImplementedError() + + def __call__(self, info, request): + if hasattr(request, 'vcs_call'): + # skip vcs calls + return + + user_id = info['match']['user_id'] + user_model = user.User() + by_id_match = user_model.get( + user_id, cache=True) + + if by_id_match: + # register this as request object we can re-use later + request.db_user = by_id_match + request.db_user_supports_default = self.supports_default + return True + + return False + + +class UserRoutePredicate(UserRoutePredicateBase): + supports_default = False + + def text(self): + return 'user_route = %s' % self.val + + phash = text + + +class UserRouteWithDefaultPredicate(UserRoutePredicateBase): + supports_default = True + + def text(self): + return 'user_with_default_route = %s' % self.val + + phash = text + + def includeme(config): config.add_route_predicate( 'repo_route', RepoRoutePredicate) @@ -503,3 +564,7 @@ def includeme(config): 'repo_group_route', RepoGroupRoutePredicate) config.add_route_predicate( 'user_group_route', UserGroupRoutePredicate) + config.add_route_predicate( + 'user_route_with_default', UserRouteWithDefaultPredicate) + config.add_route_predicate( + 'user_route', UserRoutePredicate) \ No newline at end of file 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 @@ -137,74 +137,133 @@ def admin_routes(config): name='users_data', pattern='/users_data') + config.add_route( + name='users_create', + pattern='/users/create') + + config.add_route( + name='users_new', + pattern='/users/new') + + # user management + config.add_route( + name='user_edit', + pattern='/users/{user_id:\d+}/edit', + user_route=True) + config.add_route( + name='user_edit_advanced', + pattern='/users/{user_id:\d+}/edit/advanced', + user_route=True) + config.add_route( + name='user_edit_global_perms', + pattern='/users/{user_id:\d+}/edit/global_permissions', + user_route=True) + config.add_route( + name='user_edit_global_perms_update', + pattern='/users/{user_id:\d+}/edit/global_permissions/update', + user_route=True) + config.add_route( + name='user_update', + pattern='/users/{user_id:\d+}/update', + user_route=True) + config.add_route( + name='user_delete', + pattern='/users/{user_id:\d+}/delete', + user_route=True) + config.add_route( + name='user_force_password_reset', + pattern='/users/{user_id:\d+}/password_reset', + user_route=True) + config.add_route( + name='user_create_personal_repo_group', + pattern='/users/{user_id:\d+}/create_repo_group', + user_route=True) + # user auth tokens config.add_route( name='edit_user_auth_tokens', - pattern='/users/{user_id:\d+}/edit/auth_tokens') + pattern='/users/{user_id:\d+}/edit/auth_tokens', + user_route=True) config.add_route( name='edit_user_auth_tokens_add', - pattern='/users/{user_id:\d+}/edit/auth_tokens/new') + pattern='/users/{user_id:\d+}/edit/auth_tokens/new', + user_route=True) config.add_route( name='edit_user_auth_tokens_delete', - pattern='/users/{user_id:\d+}/edit/auth_tokens/delete') + pattern='/users/{user_id:\d+}/edit/auth_tokens/delete', + user_route=True) # user ssh keys config.add_route( name='edit_user_ssh_keys', - pattern='/users/{user_id:\d+}/edit/ssh_keys') + pattern='/users/{user_id:\d+}/edit/ssh_keys', + user_route=True) config.add_route( name='edit_user_ssh_keys_generate_keypair', - pattern='/users/{user_id:\d+}/edit/ssh_keys/generate') + pattern='/users/{user_id:\d+}/edit/ssh_keys/generate', + user_route=True) config.add_route( name='edit_user_ssh_keys_add', - pattern='/users/{user_id:\d+}/edit/ssh_keys/new') + pattern='/users/{user_id:\d+}/edit/ssh_keys/new', + user_route=True) config.add_route( name='edit_user_ssh_keys_delete', - pattern='/users/{user_id:\d+}/edit/ssh_keys/delete') + pattern='/users/{user_id:\d+}/edit/ssh_keys/delete', + user_route=True) # user emails config.add_route( name='edit_user_emails', - pattern='/users/{user_id:\d+}/edit/emails') + pattern='/users/{user_id:\d+}/edit/emails', + user_route=True) config.add_route( name='edit_user_emails_add', - pattern='/users/{user_id:\d+}/edit/emails/new') + pattern='/users/{user_id:\d+}/edit/emails/new', + user_route=True) config.add_route( name='edit_user_emails_delete', - pattern='/users/{user_id:\d+}/edit/emails/delete') + pattern='/users/{user_id:\d+}/edit/emails/delete', + user_route=True) # user IPs config.add_route( name='edit_user_ips', - pattern='/users/{user_id:\d+}/edit/ips') + pattern='/users/{user_id:\d+}/edit/ips', + user_route=True) config.add_route( name='edit_user_ips_add', - pattern='/users/{user_id:\d+}/edit/ips/new') + pattern='/users/{user_id:\d+}/edit/ips/new', + user_route_with_default=True) # enabled for default user too config.add_route( name='edit_user_ips_delete', - pattern='/users/{user_id:\d+}/edit/ips/delete') + pattern='/users/{user_id:\d+}/edit/ips/delete', + user_route_with_default=True) # enabled for default user too # user perms config.add_route( name='edit_user_perms_summary', - pattern='/users/{user_id:\d+}/edit/permissions_summary') + pattern='/users/{user_id:\d+}/edit/permissions_summary', + user_route=True) config.add_route( name='edit_user_perms_summary_json', - pattern='/users/{user_id:\d+}/edit/permissions_summary/json') + pattern='/users/{user_id:\d+}/edit/permissions_summary/json', + user_route=True) # user user groups management config.add_route( name='edit_user_groups_management', - pattern='/users/{user_id:\d+}/edit/groups_management') + pattern='/users/{user_id:\d+}/edit/groups_management', + user_route=True) config.add_route( name='edit_user_groups_management_updates', - pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates') + pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates', + user_route=True) # user audit logs config.add_route( name='edit_user_audit_logs', - pattern='/users/{user_id:\d+}/edit/audit') + pattern='/users/{user_id:\d+}/edit/audit', user_route=True) # user-groups admin config.add_route( diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -19,8 +19,12 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import pytest +from sqlalchemy.orm.exc import NoResultFound -from rhodecode.model.db import User, UserApiKeys, UserEmailMap +from rhodecode.lib import auth +from rhodecode.lib import helpers as h +from rhodecode.model import validators +from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository from rhodecode.model.meta import Session from rhodecode.model.user import UserModel @@ -40,6 +44,27 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/users', 'users_data': ADMIN_PREFIX + '/users_data', + 'users_create': + ADMIN_PREFIX + '/users/create', + 'users_new': + ADMIN_PREFIX + '/users/new', + 'user_edit': + ADMIN_PREFIX + '/users/{user_id}/edit', + 'user_edit_advanced': + ADMIN_PREFIX + '/users/{user_id}/edit/advanced', + 'user_edit_global_perms': + ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions', + 'user_edit_global_perms_update': + ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update', + 'user_update': + ADMIN_PREFIX + '/users/{user_id}/update', + 'user_delete': + ADMIN_PREFIX + '/users/{user_id}/delete', + 'user_force_password_reset': + ADMIN_PREFIX + '/users/{user_id}/password_reset', + 'user_create_personal_repo_group': + ADMIN_PREFIX + '/users/{user_id}/create_repo_group', + 'edit_user_auth_tokens': ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens', 'edit_user_auth_tokens_add': @@ -60,6 +85,15 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/users/{user_id}/edit/ips/new', 'edit_user_ips_delete': ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete', + + 'edit_user_perms_summary': + ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary', + 'edit_user_perms_summary_json': + ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json', + + 'edit_user_audit_logs': + ADMIN_PREFIX + '/users/{user_id}/edit/audit', + }[name].format(**kwargs) if params: @@ -220,7 +254,8 @@ class TestAdminUsersView(TestController) def test_emails(self): self.log_user() user = User.get_by_username(TEST_USER_REGULAR_LOGIN) - response = self.app.get(route_path('edit_user_emails', user_id=user.user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user.user_id)) response.mustcontain('No additional emails specified') def test_emails_add(self, user_util): @@ -233,7 +268,8 @@ class TestAdminUsersView(TestController) params={'new_email': 'example@rhodecode.com', 'csrf_token': self.csrf_token}) - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) response.mustcontain('example@rhodecode.com') def test_emails_add_existing_email(self, user_util, user_regular): @@ -250,7 +286,8 @@ class TestAdminUsersView(TestController) assert_session_flash( response, 'This e-mail address is already taken') - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) response.mustcontain(no=[existing_email]) def test_emails_delete(self, user_util): @@ -263,7 +300,8 @@ class TestAdminUsersView(TestController) params={'new_email': 'example@rhodecode.com', 'csrf_token': self.csrf_token}) - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) response.mustcontain('example@rhodecode.com') user_email = UserEmailMap.query()\ @@ -277,5 +315,469 @@ class TestAdminUsersView(TestController) params={'del_email_id': del_email_id, 'csrf_token': self.csrf_token}) - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) - response.mustcontain(no=['example@rhodecode.com']) \ No newline at end of file + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) + response.mustcontain(no=['example@rhodecode.com']) + + + def test_create(self, request, xhr_header): + self.log_user() + username = 'newtestuser' + password = 'test12' + password_confirmation = password + name = 'name' + lastname = 'lastname' + email = 'mail@mail.com' + + self.app.get(route_path('users_new')) + + response = self.app.post(route_path('users_create'), params={ + 'username': username, + 'password': password, + 'password_confirmation': password_confirmation, + 'firstname': name, + 'active': True, + 'lastname': lastname, + 'extern_name': 'rhodecode', + 'extern_type': 'rhodecode', + 'email': email, + 'csrf_token': self.csrf_token, + }) + user_link = h.link_to( + username, + route_path( + 'user_edit', user_id=User.get_by_username(username).user_id)) + assert_session_flash(response, 'Created user %s' % (user_link,)) + + @request.addfinalizer + def cleanup(): + fixture.destroy_user(username) + Session().commit() + + new_user = User.query().filter(User.username == username).one() + + assert new_user.username == username + assert auth.check_password(password, new_user.password) + assert new_user.name == name + assert new_user.lastname == lastname + assert new_user.email == email + + response = self.app.get(route_path('users_data'), + extra_environ=xhr_header) + response.mustcontain(username) + + def test_create_err(self): + self.log_user() + username = 'new_user' + password = '' + name = 'name' + lastname = 'lastname' + email = 'errmail.com' + + self.app.get(route_path('users_new')) + + response = self.app.post(route_path('users_create'), params={ + 'username': username, + 'password': password, + 'name': name, + 'active': False, + 'lastname': lastname, + 'email': email, + 'csrf_token': self.csrf_token, + }) + + msg = validators.ValidUsername( + False, {})._messages['system_invalid_username'] + msg = h.html_escape(msg % {'username': 'new_user'}) + response.mustcontain('%s' % msg) + response.mustcontain( + 'Please enter a value') + response.mustcontain( + 'An email address must contain a' + ' single @') + + def get_user(): + Session().query(User).filter(User.username == username).one() + + with pytest.raises(NoResultFound): + get_user() + + def test_new(self): + self.log_user() + self.app.get(route_path('users_new')) + + @pytest.mark.parametrize("name, attrs", [ + ('firstname', {'firstname': 'new_username'}), + ('lastname', {'lastname': 'new_username'}), + ('admin', {'admin': True}), + ('admin', {'admin': False}), + ('extern_type', {'extern_type': 'ldap'}), + ('extern_type', {'extern_type': None}), + ('extern_name', {'extern_name': 'test'}), + ('extern_name', {'extern_name': None}), + ('active', {'active': False}), + ('active', {'active': True}), + ('email', {'email': 'some@email.com'}), + ('language', {'language': 'de'}), + ('language', {'language': 'en'}), + # ('new_password', {'new_password': 'foobar123', + # 'password_confirmation': 'foobar123'}) + ]) + def test_update(self, name, attrs, user_util): + self.log_user() + usr = user_util.create_user( + password='qweqwe', + email='testme@rhodecode.org', + extern_type='rhodecode', + extern_name='xxx', + ) + user_id = usr.user_id + Session().commit() + + params = usr.get_api_data() + cur_lang = params['language'] or 'en' + params.update({ + 'password_confirmation': '', + 'new_password': '', + 'language': cur_lang, + 'csrf_token': self.csrf_token, + }) + params.update({'new_password': ''}) + params.update(attrs) + if name == 'email': + params['emails'] = [attrs['email']] + elif name == 'extern_type': + # cannot update this via form, expected value is original one + params['extern_type'] = "rhodecode" + elif name == 'extern_name': + # cannot update this via form, expected value is original one + params['extern_name'] = 'xxx' + # special case since this user is not + # logged in yet his data is not filled + # so we use creation data + + response = self.app.post( + route_path('user_update', user_id=usr.user_id), params) + assert response.status_int == 302 + assert_session_flash(response, 'User updated successfully') + + updated_user = User.get(user_id) + updated_params = updated_user.get_api_data() + updated_params.update({'password_confirmation': ''}) + updated_params.update({'new_password': ''}) + + del params['csrf_token'] + assert params == updated_params + + def test_update_and_migrate_password( + self, autologin_user, real_crypto_backend, user_util): + + user = user_util.create_user() + temp_user = user.username + user.password = auth._RhodeCodeCryptoSha256().hash_create( + b'test123') + Session().add(user) + Session().commit() + + params = user.get_api_data() + + params.update({ + 'password_confirmation': 'qweqwe123', + 'new_password': 'qweqwe123', + 'language': 'en', + 'csrf_token': autologin_user.csrf_token, + }) + + response = self.app.post( + route_path('user_update', user_id=user.user_id), params) + assert response.status_int == 302 + assert_session_flash(response, 'User updated successfully') + + # new password should be bcrypted, after log-in and transfer + user = User.get_by_username(temp_user) + assert user.password.startswith('$') + + updated_user = User.get_by_username(temp_user) + updated_params = updated_user.get_api_data() + updated_params.update({'password_confirmation': 'qweqwe123'}) + updated_params.update({'new_password': 'qweqwe123'}) + + del params['csrf_token'] + assert params == updated_params + + def test_delete(self): + self.log_user() + username = 'newtestuserdeleteme' + + fixture.create_user(name=username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + assert_session_flash(response, 'Successfully deleted user') + + def test_delete_owner_of_repository(self, request, user_util): + self.log_user() + obj_name = 'test_repo' + usr = user_util.create_user() + username = usr.username + fixture.create_repo(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \ + 'Switch owners or remove those repositories:%s' % (username, + obj_name) + assert_session_flash(response, msg) + fixture.destroy_repo(obj_name) + + def test_delete_owner_of_repository_detaching(self, request, user_util): + self.log_user() + obj_name = 'test_repo' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repos': 'detach', 'csrf_token': self.csrf_token}) + + msg = 'Detached 1 repositories' + assert_session_flash(response, msg) + fixture.destroy_repo(obj_name) + + def test_delete_owner_of_repository_deleting(self, request, user_util): + self.log_user() + obj_name = 'test_repo' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repos': 'delete', 'csrf_token': self.csrf_token}) + + msg = 'Deleted 1 repositories' + assert_session_flash(response, msg) + + def test_delete_owner_of_repository_group(self, request, user_util): + self.log_user() + obj_name = 'test_group' + usr = user_util.create_user() + username = usr.username + fixture.create_repo_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \ + 'Switch owners or remove those repository groups:%s' % (username, + obj_name) + assert_session_flash(response, msg) + fixture.destroy_repo_group(obj_name) + + def test_delete_owner_of_repository_group_detaching(self, request, user_util): + self.log_user() + obj_name = 'test_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token}) + + msg = 'Deleted 1 repository groups' + assert_session_flash(response, msg) + + def test_delete_owner_of_repository_group_deleting(self, request, user_util): + self.log_user() + obj_name = 'test_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token}) + + msg = 'Detached 1 repository groups' + assert_session_flash(response, msg) + fixture.destroy_repo_group(obj_name) + + def test_delete_owner_of_user_group(self, request, user_util): + self.log_user() + obj_name = 'test_user_group' + usr = user_util.create_user() + username = usr.username + fixture.create_user_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \ + 'Switch owners or remove those user groups:%s' % (username, + obj_name) + assert_session_flash(response, msg) + fixture.destroy_user_group(obj_name) + + def test_delete_owner_of_user_group_detaching(self, request, user_util): + self.log_user() + obj_name = 'test_user_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_user_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + try: + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_user_groups': 'detach', + 'csrf_token': self.csrf_token}) + + msg = 'Detached 1 user groups' + assert_session_flash(response, msg) + finally: + fixture.destroy_user_group(obj_name) + + def test_delete_owner_of_user_group_deleting(self, request, user_util): + self.log_user() + obj_name = 'test_user_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_user_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token}) + + msg = 'Deleted 1 user groups' + assert_session_flash(response, msg) + + def test_edit(self, user_util): + self.log_user() + user = user_util.create_user() + self.app.get(route_path('user_edit', user_id=user.user_id)) + + def test_edit_default_user_redirect(self): + self.log_user() + user = User.get_default_user() + self.app.get(route_path('user_edit', user_id=user.user_id), status=302) + + @pytest.mark.parametrize( + 'repo_create, repo_create_write, user_group_create, repo_group_create,' + 'fork_create, inherit_default_permissions, expect_error,' + 'expect_form_error', [ + ('hg.create.none', 'hg.create.write_on_repogroup.false', + 'hg.usergroup.create.false', 'hg.repogroup.create.false', + 'hg.fork.none', 'hg.inherit_default_perms.false', False, False), + ('hg.create.repository', 'hg.create.write_on_repogroup.false', + 'hg.usergroup.create.false', 'hg.repogroup.create.false', + 'hg.fork.none', 'hg.inherit_default_perms.false', False, False), + ('hg.create.repository', 'hg.create.write_on_repogroup.true', + 'hg.usergroup.create.true', 'hg.repogroup.create.true', + 'hg.fork.repository', 'hg.inherit_default_perms.false', False, + False), + ('hg.create.XXX', 'hg.create.write_on_repogroup.true', + 'hg.usergroup.create.true', 'hg.repogroup.create.true', + 'hg.fork.repository', 'hg.inherit_default_perms.false', False, + True), + ('', '', '', '', '', '', True, False), + ]) + def test_global_perms_on_user( + self, repo_create, repo_create_write, user_group_create, + repo_group_create, fork_create, expect_error, expect_form_error, + inherit_default_permissions, user_util): + self.log_user() + user = user_util.create_user() + uid = user.user_id + + # ENABLE REPO CREATE ON A GROUP + perm_params = { + 'inherit_default_permissions': False, + 'default_repo_create': repo_create, + 'default_repo_create_on_write': repo_create_write, + 'default_user_group_create': user_group_create, + 'default_repo_group_create': repo_group_create, + 'default_fork_create': fork_create, + 'default_inherit_default_permissions': inherit_default_permissions, + 'csrf_token': self.csrf_token, + } + response = self.app.post( + route_path('user_edit_global_perms_update', user_id=uid), + params=perm_params) + + if expect_form_error: + assert response.status_int == 200 + response.mustcontain('Value must be one of') + else: + if expect_error: + msg = 'An error occurred during permissions saving' + else: + msg = 'User global permissions updated successfully' + ug = User.get(uid) + del perm_params['inherit_default_permissions'] + del perm_params['csrf_token'] + assert perm_params == ug.get_default_perms() + assert_session_flash(response, msg) + + def test_global_permissions_initial_values(self, user_util): + self.log_user() + user = user_util.create_user() + uid = user.user_id + response = self.app.get( + route_path('user_edit_global_perms', user_id=uid)) + default_user = User.get_default_user() + default_permissions = default_user.get_default_perms() + assert_response = response.assert_response() + expected_permissions = ( + 'default_repo_create', 'default_repo_create_on_write', + 'default_fork_create', 'default_repo_group_create', + 'default_user_group_create', 'default_inherit_default_permissions') + for permission in expected_permissions: + css_selector = '[name={}][checked=checked]'.format(permission) + element = assert_response.get_element(css_selector) + assert element.value == default_permissions[permission] + + def test_perms_summary_page(self): + user = self.log_user() + response = self.app.get( + route_path('edit_user_perms_summary', user_id=user['user_id'])) + for repo in Repository.query().all(): + response.mustcontain(repo.repo_name) + + def test_perms_summary_page_json(self): + user = self.log_user() + response = self.app.get( + route_path('edit_user_perms_summary_json', user_id=user['user_id'])) + for repo in Repository.query().all(): + response.mustcontain(repo.repo_name) + + def test_audit_log_page(self): + user = self.log_user() + self.app.get( + route_path('edit_user_audit_logs', user_id=user['user_id'])) 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 @@ -18,7 +18,6 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -import time import logging import datetime import formencode @@ -26,51 +25,46 @@ import formencode.htmlfill from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config -from sqlalchemy.sql.functions import coalesce -from sqlalchemy.exc import IntegrityError +from pyramid.renderers import render +from pyramid.response import Response -from rhodecode.apps._base import BaseAppView, DataGridAppView +from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView from rhodecode.apps.ssh_support import SshKeyFileChangeEvent +from rhodecode.authentication.plugins import auth_rhodecode from rhodecode.events import trigger from rhodecode.lib import audit_logger +from rhodecode.lib.exceptions import ( + UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException, + UserOwnsUserGroupsException, DefaultUserException) from rhodecode.lib.ext_json import json from rhodecode.lib.auth import ( LoginRequired, HasPermissionAllDecorator, CSRFRequired) from rhodecode.lib import helpers as h -from rhodecode.lib.utils2 import safe_int, safe_unicode +from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict from rhodecode.model.auth_token import AuthTokenModel +from rhodecode.model.forms import ( + UserForm, UserIndividualPermissionsForm, UserPermissionsForm) +from rhodecode.model.permission import PermissionModel +from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.ssh_key import SshKeyModel from rhodecode.model.user import UserModel from rhodecode.model.user_group import UserGroupModel from rhodecode.model.db import ( - or_, User, UserIpMap, UserEmailMap, UserApiKeys, UserSshKeys) + or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap, + UserApiKeys, UserSshKeys, RepoGroup) from rhodecode.model.meta import Session log = logging.getLogger(__name__) class AdminUsersView(BaseAppView, DataGridAppView): - ALLOW_SCOPED_TOKENS = False - """ - This view has alternative version inside EE, if modified please take a look - in there as well. - """ def load_default_context(self): c = self._get_local_tmpl_context() - c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS self._register_global_c(c) return c - def _redirect_for_default_user(self, username): - _ = self.request.translate - if username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - # TODO(marcink): redirect to 'users' admin panel once this - # is a pyramid view - raise HTTPFound('/') - @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( @@ -163,6 +157,529 @@ class AdminUsersView(BaseAppView, DataGr return data + def _set_personal_repo_group_template_vars(self, c_obj): + DummyUser = AttributeDict({ + 'username': '${username}', + 'user_id': '${user_id}', + }) + c_obj.default_create_repo_group = RepoGroupModel() \ + .get_default_create_personal_repo_group() + c_obj.personal_repo_group_name = RepoGroupModel() \ + .get_personal_group_name(DummyUser) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='users_new', request_method='GET', + renderer='rhodecode:templates/admin/users/user_add.mako') + def users_new(self): + _ = self.request.translate + c = self.load_default_context() + c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name + self._set_personal_repo_group_template_vars(c) + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='users_create', request_method='POST', + renderer='rhodecode:templates/admin/users/user_add.mako') + def users_create(self): + _ = self.request.translate + c = self.load_default_context() + c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name + user_model = UserModel() + user_form = UserForm()() + try: + form_result = user_form.to_python(dict(self.request.POST)) + user = user_model.create(form_result) + Session().flush() + creation_data = user.get_api_data() + username = form_result['username'] + + audit_logger.store_web( + 'user.create', action_data={'data': creation_data}, + user=c.rhodecode_user) + + user_link = h.link_to( + h.escape(username), + h.route_path('user_edit', user_id=user.user_id)) + h.flash(h.literal(_('Created user %(user_link)s') + % {'user_link': user_link}), category='success') + Session().commit() + except formencode.Invalid as errors: + self._set_personal_repo_group_template_vars(c) + data = render( + 'rhodecode:templates/admin/users/user_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except UserCreationError as e: + h.flash(e, 'error') + except Exception: + log.exception("Exception creation of user") + h.flash(_('Error occurred during creation of user %s') + % self.request.POST.get('username'), category='error') + raise HTTPFound(h.route_path('users')) + + +class UsersView(UserAppView): + ALLOW_SCOPED_TOKENS = False + """ + This view has alternative version inside EE, if modified please take a look + in there as well. + """ + + def load_default_context(self): + c = self._get_local_tmpl_context() + c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS + c.allowed_languages = [ + ('en', 'English (en)'), + ('de', 'German (de)'), + ('fr', 'French (fr)'), + ('it', 'Italian (it)'), + ('ja', 'Japanese (ja)'), + ('pl', 'Polish (pl)'), + ('pt', 'Portuguese (pt)'), + ('ru', 'Russian (ru)'), + ('zh', 'Chinese (zh)'), + ] + req = self.request + + c.available_permissions = req.registry.settings['available_permissions'] + PermissionModel().set_global_permission_choices( + c, gettext_translator=req.translate) + + self._register_global_c(c) + return c + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_update', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_update(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + c.active = 'profile' + c.extern_type = c.user.extern_type + c.extern_name = c.user.extern_name + c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr) + available_languages = [x[0] for x in c.allowed_languages] + _form = UserForm(edit=True, available_languages=available_languages, + old_data={'user_id': user_id, + 'email': c.user.email})() + form_result = {} + old_values = c.user.get_api_data() + try: + form_result = _form.to_python(dict(self.request.POST)) + skip_attrs = ['extern_type', 'extern_name'] + # TODO: plugin should define if username can be updated + if c.extern_type != "rhodecode": + # forbid updating username for external accounts + skip_attrs.append('username') + + UserModel().update_user( + user_id, skip_attrs=skip_attrs, **form_result) + + audit_logger.store_web( + 'user.edit', action_data={'old_data': old_values}, + user=c.rhodecode_user) + + Session().commit() + h.flash(_('User updated successfully'), category='success') + except formencode.Invalid as errors: + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except UserCreationError as e: + h.flash(e, 'error') + except Exception: + log.exception("Exception updating user") + h.flash(_('Error occurred during update of user %s') + % form_result.get('username'), category='error') + raise HTTPFound(h.route_path('user_edit', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_delete', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_delete(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + _repos = c.user.repositories + _repo_groups = c.user.repository_groups + _user_groups = c.user.user_groups + + handle_repos = None + handle_repo_groups = None + handle_user_groups = None + # dummy call for flash of handle + set_handle_flash_repos = lambda: None + set_handle_flash_repo_groups = lambda: None + set_handle_flash_user_groups = lambda: None + + if _repos and self.request.POST.get('user_repos'): + do = self.request.POST['user_repos'] + if do == 'detach': + handle_repos = 'detach' + set_handle_flash_repos = lambda: h.flash( + _('Detached %s repositories') % len(_repos), + category='success') + elif do == 'delete': + handle_repos = 'delete' + set_handle_flash_repos = lambda: h.flash( + _('Deleted %s repositories') % len(_repos), + category='success') + + if _repo_groups and self.request.POST.get('user_repo_groups'): + do = self.request.POST['user_repo_groups'] + if do == 'detach': + handle_repo_groups = 'detach' + set_handle_flash_repo_groups = lambda: h.flash( + _('Detached %s repository groups') % len(_repo_groups), + category='success') + elif do == 'delete': + handle_repo_groups = 'delete' + set_handle_flash_repo_groups = lambda: h.flash( + _('Deleted %s repository groups') % len(_repo_groups), + category='success') + + if _user_groups and self.request.POST.get('user_user_groups'): + do = self.request.POST['user_user_groups'] + if do == 'detach': + handle_user_groups = 'detach' + set_handle_flash_user_groups = lambda: h.flash( + _('Detached %s user groups') % len(_user_groups), + category='success') + elif do == 'delete': + handle_user_groups = 'delete' + set_handle_flash_user_groups = lambda: h.flash( + _('Deleted %s user groups') % len(_user_groups), + category='success') + + old_values = c.user.get_api_data() + try: + UserModel().delete(c.user, handle_repos=handle_repos, + handle_repo_groups=handle_repo_groups, + handle_user_groups=handle_user_groups) + + audit_logger.store_web( + 'user.delete', action_data={'old_data': old_values}, + user=c.rhodecode_user) + + Session().commit() + set_handle_flash_repos() + set_handle_flash_repo_groups() + set_handle_flash_user_groups() + h.flash(_('Successfully deleted user'), category='success') + except (UserOwnsReposException, UserOwnsRepoGroupsException, + UserOwnsUserGroupsException, DefaultUserException) as e: + h.flash(e, category='warning') + except Exception: + log.exception("Exception during deletion of user") + h.flash(_('An error occurred during deletion of user'), + category='error') + raise HTTPFound(h.route_path('users')) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='user_edit', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = 'profile' + c.extern_type = c.user.extern_type + c.extern_name = c.user.extern_name + c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr) + + defaults = c.user.get_dict() + defaults.update({'language': c.user.user_data.get('language')}) + + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='user_edit_advanced', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit_advanced(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + c.active = 'advanced' + c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id) + c.personal_repo_group_name = RepoGroupModel()\ + .get_personal_group_name(c.user) + + c.user_to_review_rules = sorted( + (x.user for x in c.user.user_review_rules), + key=lambda u: u.username.lower()) + + c.first_admin = User.get_first_super_admin() + defaults = c.user.get_dict() + + # Interim workaround if the user participated on any pull requests as a + # reviewer. + has_review = len(c.user.reviewer_pull_requests) + c.can_delete_user = not has_review + c.can_delete_user_message = '' + inactive_link = h.link_to( + 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active')) + if has_review == 1: + c.can_delete_user_message = h.literal(_( + 'The user participates as reviewer in {} pull request and ' + 'cannot be deleted. \nYou can set the user to ' + '"{}" instead of deleting it.').format( + has_review, inactive_link)) + elif has_review: + c.can_delete_user_message = h.literal(_( + 'The user participates as reviewer in {} pull requests and ' + 'cannot be deleted. \nYou can set the user to ' + '"{}" instead of deleting it.').format( + has_review, inactive_link)) + + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='user_edit_global_perms', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit_global_perms(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = 'global_perms' + + c.default_user = User.get_default_user() + defaults = c.user.get_dict() + defaults.update(c.default_user.get_default_perms(suffix='_inherited')) + defaults.update(c.default_user.get_default_perms()) + defaults.update(c.user.get_default_perms()) + + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_edit_global_perms_update', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit_global_perms_update(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + c.active = 'global_perms' + try: + # first stage that verifies the checkbox + _form = UserIndividualPermissionsForm() + form_result = _form.to_python(dict(self.request.POST)) + inherit_perms = form_result['inherit_default_permissions'] + c.user.inherit_default_permissions = inherit_perms + Session().add(c.user) + + if not inherit_perms: + # only update the individual ones if we un check the flag + _form = UserPermissionsForm( + [x[0] for x in c.repo_create_choices], + [x[0] for x in c.repo_create_on_write_choices], + [x[0] for x in c.repo_group_create_choices], + [x[0] for x in c.user_group_create_choices], + [x[0] for x in c.fork_choices], + [x[0] for x in c.inherit_default_permission_choices])() + + form_result = _form.to_python(dict(self.request.POST)) + form_result.update({'perm_user_id': c.user.user_id}) + + PermissionModel().update_user_permissions(form_result) + + # TODO(marcink): implement global permissions + # audit_log.store_web('user.edit.permissions') + + Session().commit() + h.flash(_('User global permissions updated successfully'), + category='success') + + except formencode.Invalid as errors: + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception("Exception during permissions saving") + h.flash(_('An error occurred during permissions saving'), + category='error') + raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_force_password_reset', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_force_password_reset(self): + """ + toggle reset password flag for this user + """ + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + try: + old_value = c.user.user_data.get('force_password_change') + c.user.update_userdata(force_password_change=not old_value) + + if old_value: + msg = _('Force password change disabled for user') + audit_logger.store_web( + 'user.edit.password_reset.disabled', + user=c.rhodecode_user) + else: + msg = _('Force password change enabled for user') + audit_logger.store_web( + 'user.edit.password_reset.enabled', + user=c.rhodecode_user) + + Session().commit() + h.flash(msg, category='success') + except Exception: + log.exception("Exception during password reset for user") + h.flash(_('An error occurred during password reset for user'), + category='error') + + raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_create_personal_repo_group', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_create_personal_repo_group(self): + """ + Create personal repository group for this user + """ + from rhodecode.model.repo_group import RepoGroupModel + + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + personal_repo_group = RepoGroup.get_user_personal_repo_group( + c.user.user_id) + if personal_repo_group: + raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id)) + + personal_repo_group_name = RepoGroupModel().get_personal_group_name( + c.user) + named_personal_group = RepoGroup.get_by_group_name( + personal_repo_group_name) + try: + + if named_personal_group and named_personal_group.user_id == c.user.user_id: + # migrate the same named group, and mark it as personal + named_personal_group.personal = True + Session().add(named_personal_group) + Session().commit() + msg = _('Linked repository group `%s` as personal' % ( + personal_repo_group_name,)) + h.flash(msg, category='success') + elif not named_personal_group: + RepoGroupModel().create_personal_repo_group(c.user) + + msg = _('Created repository group `%s`' % ( + personal_repo_group_name,)) + h.flash(msg, category='success') + else: + msg = _('Repository group `%s` is already taken' % ( + personal_repo_group_name,)) + h.flash(msg, category='warning') + except Exception: + log.exception("Exception during repository group creation") + msg = _( + 'An error occurred during repository group creation for user') + h.flash(msg, category='error') + Session().rollback() + + raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id)) + @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( @@ -171,10 +688,7 @@ class AdminUsersView(BaseAppView, DataGr def auth_tokens(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'auth_tokens' @@ -200,10 +714,8 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user user_data = c.user.get_api_data() lifetime = safe_int(self.request.POST.get('lifetime'), -1) @@ -233,9 +745,9 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user + user_data = c.user.get_api_data() del_auth_token = self.request.POST.get('del_auth_token') @@ -262,10 +774,7 @@ class AdminUsersView(BaseAppView, DataGr def ssh_keys(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'ssh_keys' c.default_key = self.request.GET.get('default_key') @@ -281,9 +790,7 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'ssh_keys_generate' comment = 'RhodeCode-SSH {}'.format(c.user.email or '') @@ -300,10 +807,8 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user user_data = c.user.get_api_data() key_data = self.request.POST.get('key_data') @@ -353,9 +858,9 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user + user_data = c.user.get_api_data() del_ssh_key = self.request.POST.get('del_ssh_key') @@ -384,10 +889,7 @@ class AdminUsersView(BaseAppView, DataGr def emails(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'emails' c.user_email_map = UserEmailMap.query() \ @@ -404,22 +906,26 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user email = self.request.POST.get('new_email') user_data = c.user.get_api_data() try: UserModel().add_extra_email(c.user.user_id, email) audit_logger.store_web( - 'user.edit.email.add', action_data={'email': email, 'user': user_data}, + 'user.edit.email.add', + action_data={'email': email, 'user': user_data}, user=self._rhodecode_user) Session().commit() h.flash(_("Added new email address `%s` for user account") % email, category='success') except formencode.Invalid as error: h.flash(h.escape(error.error_dict['email']), category='error') + except IntegrityError: + log.warning("Email %s already exists", email) + h.flash(_('Email `{}` is already registered for another user.').format(email), + category='error') except Exception: log.exception("Exception during email saving") h.flash(_('An error occurred during email saving'), @@ -435,9 +941,8 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user email_id = self.request.POST.get('del_email_id') user_model = UserModel() @@ -446,7 +951,8 @@ class AdminUsersView(BaseAppView, DataGr user_data = c.user.get_api_data() user_model.delete_extra_email(c.user.user_id, email_id) audit_logger.store_web( - 'user.edit.email.delete', action_data={'email': email, 'user': user_data}, + 'user.edit.email.delete', + action_data={'email': email, 'user': user_data}, user=self._rhodecode_user) Session().commit() h.flash(_("Removed email address from user account"), @@ -461,10 +967,7 @@ class AdminUsersView(BaseAppView, DataGr def ips(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'ips' c.user_ip_map = UserIpMap.query() \ @@ -481,14 +984,14 @@ class AdminUsersView(BaseAppView, DataGr @CSRFRequired() @view_config( route_name='edit_user_ips_add', request_method='POST') + # NOTE(marcink): this view is allowed for default users, as we can + # edit their IP white list def ips_add(self): _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - # NOTE(marcink): this view is allowed for default users, as we can - # edit their IP white list + user_id = self.db_user_id + c.user = self.db_user user_model = UserModel() desc = self.request.POST.get('description') @@ -506,7 +1009,8 @@ class AdminUsersView(BaseAppView, DataGr try: user_model.add_extra_ip(c.user.user_id, ip, desc) audit_logger.store_web( - 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data}, + 'user.edit.ip.add', + action_data={'ip': ip, 'user': user_data}, user=self._rhodecode_user) Session().commit() added.append(ip) @@ -531,14 +1035,14 @@ class AdminUsersView(BaseAppView, DataGr @CSRFRequired() @view_config( route_name='edit_user_ips_delete', request_method='POST') + # NOTE(marcink): this view is allowed for default users, as we can + # edit their IP white list def ips_delete(self): _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - # NOTE(marcink): this view is allowed for default users, as we can - # edit their IP white list + user_id = self.db_user_id + c.user = self.db_user ip_id = self.request.POST.get('del_ip_id') user_model = UserModel() @@ -563,11 +1067,9 @@ class AdminUsersView(BaseAppView, DataGr renderer='rhodecode:templates/admin/users/user_edit.mako') def groups_management(self): c = self.load_default_context() + c.user = self.db_user + c.data = c.user.group_member - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - c.data = c.user.group_member - self._redirect_for_default_user(c.user.username) groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member] c.groups = json.dumps(groups) @@ -584,9 +1086,8 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user user_groups = set(self.request.POST.getall('users_group_id')) user_groups_objects = [] @@ -595,7 +1096,25 @@ class AdminUsersView(BaseAppView, DataGr user_groups_objects.append( UserGroupModel().get_group(safe_int(ugid))) user_group_model = UserGroupModel() - user_group_model.change_groups(c.user, user_groups_objects) + added_to_groups, removed_from_groups = \ + user_group_model.change_groups(c.user, user_groups_objects) + + user_data = c.user.get_api_data() + for user_group_id in added_to_groups: + user_group = UserGroup.get(user_group_id) + old_values = user_group.get_api_data() + audit_logger.store_web( + 'user_group.edit.member.add', + action_data={'user': user_data, 'old_data': old_values}, + user=self._rhodecode_user) + + for user_group_id in removed_from_groups: + user_group = UserGroup.get(user_group_id) + old_values = user_group.get_api_data() + audit_logger.store_web( + 'user_group.edit.member.delete', + action_data={'user': user_data, 'old_data': old_values}, + user=self._rhodecode_user) Session().commit() c.active = 'user_groups_management' @@ -612,10 +1131,8 @@ class AdminUsersView(BaseAppView, DataGr def user_audit_logs(self): _ = self.request.translate c = self.load_default_context() + c.user = self.db_user - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) c.active = 'audit' p = safe_int(self.request.GET.get('page', 1), 1) @@ -641,10 +1158,7 @@ class AdminUsersView(BaseAppView, DataGr def user_perms_summary(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'perms_summary' c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr) @@ -658,11 +1172,6 @@ class AdminUsersView(BaseAppView, DataGr renderer='json_ext') def user_perms_summary_json(self): self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - user = User.get_or_404(user_id) - self._redirect_for_default_user(user.username) - - perm_user = user.AuthUser(ip_addr=self.request.remote_addr) + perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr) return perm_user.permissions diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -115,7 +115,6 @@ def load_environment(global_conf, app_co 'secret': config.get('channelstream.secret') } - set_available_permissions(config) db_cfg = make_db_config(clear_session=True) repos_path = list(db_cfg.items('paths'))[0][1] @@ -178,5 +177,6 @@ def load_pyramid_environment(global_conf log_level=settings['vcs.server.log_level']) utils.configure_vcs(settings) + if vcs_server_enabled: connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings)) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -41,6 +41,7 @@ import rhodecode from rhodecode.model import meta from rhodecode.config import patches +from rhodecode.config import utils as config_utils from rhodecode.config.routing import STATIC_FILE_PREFIX from rhodecode.config.environment import ( load_environment, load_pyramid_environment) @@ -56,7 +57,7 @@ from rhodecode.lib.plugins.utils import from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict from rhodecode.subscribers import ( scan_repositories_if_enabled, write_js_routes_if_enabled, - write_metadata_if_needed) + write_metadata_if_needed, inject_app_settings) log = logging.getLogger(__name__) @@ -146,11 +147,12 @@ def make_pyramid_app(global_config, **se settings_pylons = settings.copy() sanitize_settings_and_apply_defaults(settings) + config = Configurator(settings=settings) + load_pyramid_environment(global_config, settings) + add_pylons_compat_data(config.registry, global_config, settings_pylons) - load_pyramid_environment(global_config, settings) - includeme_first(config) includeme(config) @@ -315,6 +317,7 @@ def includeme(config): settings['default_locale_name'] = settings.get('lang', 'en') # Add subscribers. + config.add_subscriber(inject_app_settings, ApplicationCreated) config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated) config.add_subscriber(write_metadata_if_needed, ApplicationCreated) config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated) @@ -472,6 +475,9 @@ def sanitize_settings_and_apply_defaults _sanitize_appenlight_settings(settings) _sanitize_vcs_settings(settings) + # configure instance id + config_utils.set_instance_id(settings) + return settings diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -219,37 +219,6 @@ def make_map(config): 'function': check_group}, requirements=URL_NAME_REQUIREMENTS) - # ADMIN USER ROUTES - with rmap.submapper(path_prefix=ADMIN_PREFIX, - controller='admin/users') as m: - m.connect('users', '/users', - action='create', conditions={'method': ['POST']}) - m.connect('new_user', '/users/new', - action='new', conditions={'method': ['GET']}) - m.connect('update_user', '/users/{user_id}', - action='update', conditions={'method': ['PUT']}) - m.connect('delete_user', '/users/{user_id}', - action='delete', conditions={'method': ['DELETE']}) - m.connect('edit_user', '/users/{user_id}/edit', - action='edit', conditions={'method': ['GET']}, jsroute=True) - m.connect('user', '/users/{user_id}', - action='show', conditions={'method': ['GET']}) - m.connect('force_password_reset_user', '/users/{user_id}/password_reset', - action='reset_password', conditions={'method': ['POST']}) - m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group', - action='create_personal_repo_group', conditions={'method': ['POST']}) - - # EXTRAS USER ROUTES - m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced', - action='edit_advanced', conditions={'method': ['GET']}) - m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced', - action='update_advanced', conditions={'method': ['PUT']}) - - m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions', - action='edit_global_perms', conditions={'method': ['GET']}) - m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions', - action='update_global_perms', conditions={'method': ['PUT']}) - # ADMIN SETTINGS ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/settings') as m: @@ -339,5 +308,4 @@ def make_map(config): m.connect('my_account_password', '/my_account/password', action='my_account_password', conditions={'method': ['GET']}) - return rmap diff --git a/rhodecode/controllers/admin/users.py b/rhodecode/controllers/admin/users.py deleted file mode 100644 --- a/rhodecode/controllers/admin/users.py +++ /dev/null @@ -1,496 +0,0 @@ -# -*- 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/ - -""" -Users crud controller for pylons -""" - -import logging -import formencode - -from formencode import htmlfill -from pylons import request, tmpl_context as c, url, config -from pylons.controllers.util import redirect -from pylons.i18n.translation import _ - -from rhodecode.authentication.plugins import auth_rhodecode - -from rhodecode.lib import helpers as h -from rhodecode.lib import auth -from rhodecode.lib import audit_logger -from rhodecode.lib.auth import ( - LoginRequired, HasPermissionAllDecorator, AuthUser) -from rhodecode.lib.base import BaseController, render -from rhodecode.lib.exceptions import ( - DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, - UserOwnsUserGroupsException, UserCreationError) -from rhodecode.lib.utils2 import safe_int, AttributeDict - -from rhodecode.model.db import ( - PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) -from rhodecode.model.forms import ( - UserForm, UserPermissionsForm, UserIndividualPermissionsForm) -from rhodecode.model.repo_group import RepoGroupModel -from rhodecode.model.user import UserModel -from rhodecode.model.meta import Session -from rhodecode.model.permission import PermissionModel - -log = logging.getLogger(__name__) - - -class UsersController(BaseController): - """REST Controller styled on the Atom Publishing Protocol""" - - @LoginRequired() - def __before__(self): - super(UsersController, self).__before__() - c.available_permissions = config['available_permissions'] - c.allowed_languages = [ - ('en', 'English (en)'), - ('de', 'German (de)'), - ('fr', 'French (fr)'), - ('it', 'Italian (it)'), - ('ja', 'Japanese (ja)'), - ('pl', 'Polish (pl)'), - ('pt', 'Portuguese (pt)'), - ('ru', 'Russian (ru)'), - ('zh', 'Chinese (zh)'), - ] - PermissionModel().set_global_permission_choices(c, gettext_translator=_) - - def _get_personal_repo_group_template_vars(self): - DummyUser = AttributeDict({ - 'username': '${username}', - 'user_id': '${user_id}', - }) - c.default_create_repo_group = RepoGroupModel() \ - .get_default_create_personal_repo_group() - c.personal_repo_group_name = RepoGroupModel() \ - .get_personal_group_name(DummyUser) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def create(self): - c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name - user_model = UserModel() - user_form = UserForm()() - try: - form_result = user_form.to_python(dict(request.POST)) - user = user_model.create(form_result) - Session().flush() - creation_data = user.get_api_data() - username = form_result['username'] - - audit_logger.store_web( - 'user.create', action_data={'data': creation_data}, - user=c.rhodecode_user) - - user_link = h.link_to(h.escape(username), - url('edit_user', - user_id=user.user_id)) - h.flash(h.literal(_('Created user %(user_link)s') - % {'user_link': user_link}), category='success') - Session().commit() - except formencode.Invalid as errors: - self._get_personal_repo_group_template_vars() - return htmlfill.render( - render('admin/users/user_add.mako'), - defaults=errors.value, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - except UserCreationError as e: - h.flash(e, 'error') - except Exception: - log.exception("Exception creation of user") - h.flash(_('Error occurred during creation of user %s') - % request.POST.get('username'), category='error') - return redirect(h.route_path('users')) - - @HasPermissionAllDecorator('hg.admin') - def new(self): - c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name - self._get_personal_repo_group_template_vars() - return render('admin/users/user_add.mako') - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def update(self, user_id): - - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - c.active = 'profile' - c.extern_type = c.user.extern_type - c.extern_name = c.user.extern_name - c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) - available_languages = [x[0] for x in c.allowed_languages] - _form = UserForm(edit=True, available_languages=available_languages, - old_data={'user_id': user_id, - 'email': c.user.email})() - form_result = {} - old_values = c.user.get_api_data() - try: - form_result = _form.to_python(dict(request.POST)) - skip_attrs = ['extern_type', 'extern_name'] - # TODO: plugin should define if username can be updated - if c.extern_type != "rhodecode": - # forbid updating username for external accounts - skip_attrs.append('username') - - UserModel().update_user( - user_id, skip_attrs=skip_attrs, **form_result) - - audit_logger.store_web( - 'user.edit', action_data={'old_data': old_values}, - user=c.rhodecode_user) - - Session().commit() - h.flash(_('User updated successfully'), category='success') - except formencode.Invalid as errors: - defaults = errors.value - e = errors.error_dict or {} - - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - errors=e, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - except UserCreationError as e: - h.flash(e, 'error') - except Exception: - log.exception("Exception updating user") - h.flash(_('Error occurred during update of user %s') - % form_result.get('username'), category='error') - return redirect(url('edit_user', user_id=user_id)) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def delete(self, user_id): - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - - _repos = c.user.repositories - _repo_groups = c.user.repository_groups - _user_groups = c.user.user_groups - - handle_repos = None - handle_repo_groups = None - handle_user_groups = None - # dummy call for flash of handle - set_handle_flash_repos = lambda: None - set_handle_flash_repo_groups = lambda: None - set_handle_flash_user_groups = lambda: None - - if _repos and request.POST.get('user_repos'): - do = request.POST['user_repos'] - if do == 'detach': - handle_repos = 'detach' - set_handle_flash_repos = lambda: h.flash( - _('Detached %s repositories') % len(_repos), - category='success') - elif do == 'delete': - handle_repos = 'delete' - set_handle_flash_repos = lambda: h.flash( - _('Deleted %s repositories') % len(_repos), - category='success') - - if _repo_groups and request.POST.get('user_repo_groups'): - do = request.POST['user_repo_groups'] - if do == 'detach': - handle_repo_groups = 'detach' - set_handle_flash_repo_groups = lambda: h.flash( - _('Detached %s repository groups') % len(_repo_groups), - category='success') - elif do == 'delete': - handle_repo_groups = 'delete' - set_handle_flash_repo_groups = lambda: h.flash( - _('Deleted %s repository groups') % len(_repo_groups), - category='success') - - if _user_groups and request.POST.get('user_user_groups'): - do = request.POST['user_user_groups'] - if do == 'detach': - handle_user_groups = 'detach' - set_handle_flash_user_groups = lambda: h.flash( - _('Detached %s user groups') % len(_user_groups), - category='success') - elif do == 'delete': - handle_user_groups = 'delete' - set_handle_flash_user_groups = lambda: h.flash( - _('Deleted %s user groups') % len(_user_groups), - category='success') - - old_values = c.user.get_api_data() - try: - UserModel().delete(c.user, handle_repos=handle_repos, - handle_repo_groups=handle_repo_groups, - handle_user_groups=handle_user_groups) - - audit_logger.store_web( - 'user.delete', action_data={'old_data': old_values}, - user=c.rhodecode_user) - - Session().commit() - set_handle_flash_repos() - set_handle_flash_repo_groups() - set_handle_flash_user_groups() - h.flash(_('Successfully deleted user'), category='success') - except (UserOwnsReposException, UserOwnsRepoGroupsException, - UserOwnsUserGroupsException, DefaultUserException) as e: - h.flash(e, category='warning') - except Exception: - log.exception("Exception during deletion of user") - h.flash(_('An error occurred during deletion of user'), - category='error') - return redirect(h.route_path('users')) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def reset_password(self, user_id): - """ - toggle reset password flag for this user - """ - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - try: - old_value = c.user.user_data.get('force_password_change') - c.user.update_userdata(force_password_change=not old_value) - - if old_value: - msg = _('Force password change disabled for user') - audit_logger.store_web( - 'user.edit.password_reset.disabled', - user=c.rhodecode_user) - else: - msg = _('Force password change enabled for user') - audit_logger.store_web( - 'user.edit.password_reset.enabled', - user=c.rhodecode_user) - - Session().commit() - h.flash(msg, category='success') - except Exception: - log.exception("Exception during password reset for user") - h.flash(_('An error occurred during password reset for user'), - category='error') - - return redirect(url('edit_user_advanced', user_id=user_id)) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def create_personal_repo_group(self, user_id): - """ - Create personal repository group for this user - """ - from rhodecode.model.repo_group import RepoGroupModel - - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - personal_repo_group = RepoGroup.get_user_personal_repo_group( - c.user.user_id) - if personal_repo_group: - return redirect(url('edit_user_advanced', user_id=user_id)) - - personal_repo_group_name = RepoGroupModel().get_personal_group_name( - c.user) - named_personal_group = RepoGroup.get_by_group_name( - personal_repo_group_name) - try: - - if named_personal_group and named_personal_group.user_id == c.user.user_id: - # migrate the same named group, and mark it as personal - named_personal_group.personal = True - Session().add(named_personal_group) - Session().commit() - msg = _('Linked repository group `%s` as personal' % ( - personal_repo_group_name,)) - h.flash(msg, category='success') - elif not named_personal_group: - RepoGroupModel().create_personal_repo_group(c.user) - - msg = _('Created repository group `%s`' % ( - personal_repo_group_name,)) - h.flash(msg, category='success') - else: - msg = _('Repository group `%s` is already taken' % ( - personal_repo_group_name,)) - h.flash(msg, category='warning') - except Exception: - log.exception("Exception during repository group creation") - msg = _( - 'An error occurred during repository group creation for user') - h.flash(msg, category='error') - Session().rollback() - - return redirect(url('edit_user_advanced', user_id=user_id)) - - @HasPermissionAllDecorator('hg.admin') - def show(self, user_id): - """GET /users/user_id: Show a specific item""" - # url('user', user_id=ID) - User.get_or_404(-1) - - @HasPermissionAllDecorator('hg.admin') - def edit(self, user_id): - """GET /users/user_id/edit: Form to edit an existing item""" - # url('edit_user', user_id=ID) - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - if c.user.username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - return redirect(h.route_path('users')) - - c.active = 'profile' - c.extern_type = c.user.extern_type - c.extern_name = c.user.extern_name - c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) - - defaults = c.user.get_dict() - defaults.update({'language': c.user.user_data.get('language')}) - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasPermissionAllDecorator('hg.admin') - def edit_advanced(self, user_id): - user_id = safe_int(user_id) - user = c.user = User.get_or_404(user_id) - if user.username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - return redirect(h.route_path('users')) - - c.active = 'advanced' - c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id) - c.personal_repo_group_name = RepoGroupModel()\ - .get_personal_group_name(user) - - c.user_to_review_rules = sorted( - (x.user for x in c.user.user_review_rules), - key=lambda u: u.username.lower()) - - c.first_admin = User.get_first_super_admin() - defaults = user.get_dict() - - # Interim workaround if the user participated on any pull requests as a - # reviewer. - has_review = len(user.reviewer_pull_requests) - c.can_delete_user = not has_review - c.can_delete_user_message = '' - inactive_link = h.link_to( - 'inactive', h.url('edit_user', user_id=user_id, anchor='active')) - if has_review == 1: - c.can_delete_user_message = h.literal(_( - 'The user participates as reviewer in {} pull request and ' - 'cannot be deleted. \nYou can set the user to ' - '"{}" instead of deleting it.').format( - has_review, inactive_link)) - elif has_review: - c.can_delete_user_message = h.literal(_( - 'The user participates as reviewer in {} pull requests and ' - 'cannot be deleted. \nYou can set the user to ' - '"{}" instead of deleting it.').format( - has_review, inactive_link)) - - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasPermissionAllDecorator('hg.admin') - def edit_global_perms(self, user_id): - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - if c.user.username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - return redirect(h.route_path('users')) - - c.active = 'global_perms' - - c.default_user = User.get_default_user() - defaults = c.user.get_dict() - defaults.update(c.default_user.get_default_perms(suffix='_inherited')) - defaults.update(c.default_user.get_default_perms()) - defaults.update(c.user.get_default_perms()) - - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def update_global_perms(self, user_id): - user_id = safe_int(user_id) - user = User.get_or_404(user_id) - c.active = 'global_perms' - try: - # first stage that verifies the checkbox - _form = UserIndividualPermissionsForm() - form_result = _form.to_python(dict(request.POST)) - inherit_perms = form_result['inherit_default_permissions'] - user.inherit_default_permissions = inherit_perms - Session().add(user) - - if not inherit_perms: - # only update the individual ones if we un check the flag - _form = UserPermissionsForm( - [x[0] for x in c.repo_create_choices], - [x[0] for x in c.repo_create_on_write_choices], - [x[0] for x in c.repo_group_create_choices], - [x[0] for x in c.user_group_create_choices], - [x[0] for x in c.fork_choices], - [x[0] for x in c.inherit_default_permission_choices])() - - form_result = _form.to_python(dict(request.POST)) - form_result.update({'perm_user_id': user.user_id}) - - PermissionModel().update_user_permissions(form_result) - - # TODO(marcink): implement global permissions - # audit_log.store_web('user.edit.permissions') - - Session().commit() - h.flash(_('User global permissions updated successfully'), - category='success') - - except formencode.Invalid as errors: - defaults = errors.value - c.user = user - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - except Exception: - log.exception("Exception during permissions saving") - h.flash(_('An error occurred during permissions saving'), - category='error') - return redirect(url('edit_user_global_perms', user_id=user_id)) - - diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1012,13 +1012,16 @@ class AuthUser(object): log.debug('No data in %s that could been used to log in', self) if not is_user_loaded: - log.debug('Failed to load user. Fallback to default user') + log.debug( + 'Failed to load user. Fallback to default user %s', anon_user) # if we cannot authenticate user try anonymous if anon_user.active: + log.debug('default user is active, using it as a session user') user_model.fill_data(self, user_id=anon_user.user_id) # then we set this user is logged in self.is_authenticated = True else: + log.debug('default user is NOT active') # in case of disabled anonymous user we reset some of the # parameters so such user is "corrupted", skipping the fill_data for attr in ['user_id', 'username', 'admin', 'active']: @@ -1277,25 +1280,26 @@ class AuthUser(object): return _set or set(['0.0.0.0/0', '::/0']) -def set_available_permissions(config): +def set_available_permissions(settings): """ - This function will propagate pylons globals with all available defined + This function will propagate pyramid settings with all available defined permission given in db. We don't want to check each time from db for new permissions since adding a new permission also requires application restart ie. to decorate new views with the newly created permission - :param config: current pylons config instance + :param settings: current pyramid registry.settings """ - log.info('getting information about all available permissions') + log.debug('auth: getting information about all available permissions') try: sa = meta.Session all_perms = sa.query(Permission).all() - config['available_permissions'] = [x.permission_name for x in all_perms] + settings.setdefault('available_permissions', + [x.permission_name for x in all_perms]) + log.debug('auth: set available permissions') except Exception: - log.error(traceback.format_exc()) - finally: - meta.Session.remove() + log.exception('Failed to fetch permissions from the database.') + raise def get_csrf_token(session, force_new=False, save_if_missing=True): diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -681,6 +681,17 @@ class User(Base, BaseModel): return 'NO_FEED_TOKEN_AVAILABLE' @classmethod + def get(cls, user_id, cache=False): + if not user_id: + return + + user = cls.query() + if cache: + user = user.options( + FromCache("sql_cache_short", "get_users_%s" % user_id)) + return user.get(user_id) + + @classmethod def extra_valid_auth_tokens(cls, user, role=None): tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\ .filter(or_(UserApiKeys.expires == -1, @@ -1341,6 +1352,9 @@ class UserGroup(Base, BaseModel): @classmethod def get(cls, user_group_id, cache=False): + if not user_group_id: + return + user_group = cls.query() if cache: user_group = user_group.options( diff --git a/rhodecode/model/user_group.py b/rhodecode/model/user_group.py --- a/rhodecode/model/user_group.py +++ b/rhodecode/model/user_group.py @@ -554,16 +554,22 @@ class UserGroupModel(BaseModel): groups_to_remove = current_groups - groups groups_to_add = groups - current_groups + removed_from_groups = [] + added_to_groups = [] for gr in groups_to_remove: log.debug('Removing user %s from user group %s', user.username, gr.users_group_name) + removed_from_groups.append(gr.users_group_id) self.remove_user_from_group(gr.users_group_name, user.username) for gr in groups_to_add: log.debug('Adding user %s to user group %s', user.username, gr.users_group_name) + added_to_groups.append(gr.users_group_id) UserGroupModel().add_user_to_group( gr.users_group_name, user.username) + return added_to_groups, removed_from_groups + def _serialize_user_group(self, user_group): import rhodecode.lib.helpers as h return { 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 @@ -12,7 +12,6 @@ ******************************************************************************/ function registerRCRoutes() { // routes registration - pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']); pyroutes.register('favicon', '/favicon.ico', []); pyroutes.register('robots', '/robots.txt', []); pyroutes.register('auth_home', '/_admin/auth*traverse', []); @@ -66,6 +65,16 @@ function registerRCRoutes() { pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []); pyroutes.register('users', '/_admin/users', []); pyroutes.register('users_data', '/_admin/users_data', []); + pyroutes.register('users_create', '/_admin/users/create', []); + pyroutes.register('users_new', '/_admin/users/new', []); + pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']); + pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']); + pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']); + pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']); + pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']); + pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']); + pyroutes.register('user_force_password_reset', '/_admin/users/%(user_id)s/password_reset', ['user_id']); + pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']); pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']); pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']); pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']); diff --git a/rhodecode/subscribers.py b/rhodecode/subscribers.py --- a/rhodecode/subscribers.py +++ b/rhodecode/subscribers.py @@ -35,6 +35,7 @@ from threading import Thread from rhodecode.translation import _ as tsf from rhodecode.config.jsroutes import generate_jsroutes_content +from rhodecode.lib import auth import rhodecode @@ -140,6 +141,12 @@ def add_pylons_context(event): pylons.tmpl_context._push_object(context) +def inject_app_settings(event): + settings = event.app.registry.settings + # inject info about available permissions + auth.set_available_permissions(settings) + + def scan_repositories_if_enabled(event): """ This is subscribed to the `pyramid.events.ApplicationCreated` event. It diff --git a/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako b/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako --- a/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako +++ b/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako @@ -102,7 +102,6 @@ var select2Options = { }; $("#role").select2(select2Options); - var preloadData = { results: [ % for entry in c.lifetime_values: 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 @@ -77,7 +77,7 @@
${base.gravatar(user.email, 16)} - ${h.link_to(h.person(user), h.url( 'edit_user',user_id=user.user_id))} + ${h.link_to(h.person(user), h.route_path('user_edit',user_id=user.user_id))} @@ -134,7 +134,7 @@ function addMember(user, fromUserGroup) { var gravatar = user.icon_link; var username = user.value_display; - var userLink = pyroutes.url('edit_user', {"user_id": user.id}); + var userLink = pyroutes.url('user_edit', {"user_id": user.id}); var uid = user.id; if (fromUserGroup) { diff --git a/rhodecode/templates/admin/users/user_add.mako b/rhodecode/templates/admin/users/user_add.mako --- a/rhodecode/templates/admin/users/user_add.mako +++ b/rhodecode/templates/admin/users/user_add.mako @@ -26,7 +26,7 @@ ${self.breadcrumbs()}
- ${h.secure_form(h.url('users'))} + ${h.secure_form(h.route_path('users_create'), request=request)}
diff --git a/rhodecode/templates/admin/users/user_edit.mako b/rhodecode/templates/admin/users/user_edit.mako --- a/rhodecode/templates/admin/users/user_edit.mako +++ b/rhodecode/templates/admin/users/user_edit.mako @@ -35,11 +35,11 @@