diff --git a/.bumpversion.cfg b/.bumpversion.cfg --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 5.2.1 +current_version = 5.3.0 message = release: Bump version {current_version} to {new_version} [bumpversion:file:rhodecode/VERSION] diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -71,6 +71,9 @@ use = egg:rhodecode-enterprise-ce ; enable proxy prefix middleware, defined above #filter-with = proxy-prefix +; control if environmental variables to be expanded into the .ini settings +#rhodecode.env_expand = true + ; ############# ; DEBUG OPTIONS ; ############# diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -71,6 +71,9 @@ use = egg:rhodecode-enterprise-ce ; enable proxy prefix middleware, defined above #filter-with = proxy-prefix +; control if environmental variables to be expanded into the .ini settings +#rhodecode.env_expand = true + ; encryption key used to encrypt social plugin tokens, ; remote_urls with credentials etc, if not set it defaults to ; `beaker.session.secret` diff --git a/docs/release-notes/release-notes-5.2.1.rst b/docs/release-notes/release-notes-5.2.1.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-5.2.1.rst @@ -0,0 +1,40 @@ +|RCE| 5.2.1 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + +- 2024-09-16 + + +New Features +^^^^^^^^^^^^ + + + +General +^^^^^^^ + + + +Security +^^^^^^^^ + + + +Performance +^^^^^^^^^^^ + + + + +Fixes +^^^^^ + +- Fixed problems with incorrect user agent errors + + +Upgrade notes +^^^^^^^^^^^^^ + +- RhodeCode 5.2.1 is unscheduled bugfix release to address some build issues with 5.2 images diff --git a/docs/release-notes/release-notes-5.3.0.rst b/docs/release-notes/release-notes-5.3.0.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-5.3.0.rst @@ -0,0 +1,45 @@ +|RCE| 5.3.0 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + +- 2024-09-17 + + +New Features +^^^^^^^^^^^^ + +- System-info: expose rhodecode config for better visibility of set settings for RhodeCode system. + + +General +^^^^^^^ + + + +Security +^^^^^^^^ + +- Permissions: fixed security problem with apply-to-children from a repo group functionality breaking + permissions for private repositories exposing them despite repo being private. +- Git-lfs: fixed security problem with allowing off-chain attacks to replace OID data without validating hash for already present oids. + This allowed to replace an LFS OID content with malicious request tailored to open RhodeCode server. + + +Performance +^^^^^^^^^^^ + + + + +Fixes +^^^^^ + +- Fixed problems with incorrect user agent errors + + +Upgrade notes +^^^^^^^^^^^^^ + +- RhodeCode 5.3.0 is unscheduled security release to address some build issues with 5.X images diff --git a/docs/release-notes/release-notes.rst b/docs/release-notes/release-notes.rst --- a/docs/release-notes/release-notes.rst +++ b/docs/release-notes/release-notes.rst @@ -9,6 +9,8 @@ Release Notes .. toctree:: :maxdepth: 1 + release-notes-5.3.0.rst + release-notes-5.2.1.rst release-notes-5.2.0.rst release-notes-5.1.2.rst release-notes-5.1.1.rst diff --git a/rhodecode/VERSION b/rhodecode/VERSION --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1,1 +1,1 @@ -5.2.1 \ No newline at end of file +5.3.0 diff --git a/rhodecode/apps/admin/views/system_info.py b/rhodecode/apps/admin/views/system_info.py --- a/rhodecode/apps/admin/views/system_info.py +++ b/rhodecode/apps/admin/views/system_info.py @@ -199,8 +199,12 @@ class AdminSystemInfoSettingsView(BaseAp ] + c.rhodecode_data_items = [ + (k, v) for k, v in sorted((val('rhodecode_server_config') or {}).items(), key=lambda x: x[0].lower()) + ] + c.vcsserver_data_items = [ - (k, v) for k, v in (val('vcs_server_config') or {}).items() + (k, v) for k, v in sorted((val('vcs_server_config') or {}).items(), key=lambda x: x[0].lower()) ] if snapshot: diff --git a/rhodecode/apps/repository/views/repo_permissions.py b/rhodecode/apps/repository/views/repo_permissions.py --- a/rhodecode/apps/repository/views/repo_permissions.py +++ b/rhodecode/apps/repository/views/repo_permissions.py @@ -100,22 +100,16 @@ class RepoSettingsPermissionsView(RepoAp self.load_default_context() private_flag = str2bool(self.request.POST.get('private')) - + changes = { + 'repo_private': private_flag + } try: repo = RepoModel().get(self.db_repo.repo_id) - repo.private = private_flag - Session().add(repo) - RepoModel().grant_user_permission( - repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none' - ) - + RepoModel().update(repo, **changes) Session().commit() h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name), category='success') - # NOTE(dan): we change repo private mode we need to notify all USERS - affected_user_ids = User.get_all_user_ids() - PermissionModel().trigger_permission_flush(affected_user_ids) except Exception: log.exception("Exception during update of repository") diff --git a/rhodecode/config/settings_maker.py b/rhodecode/config/settings_maker.py --- a/rhodecode/config/settings_maker.py +++ b/rhodecode/config/settings_maker.py @@ -109,6 +109,9 @@ class SettingsMaker: return envvar_value def env_expand(self): + if self.settings.get('rhodecode.env_expand') == 'false': + return + replaced = {} for k, v in self.settings.items(): if k not in set_keys: diff --git a/rhodecode/lib/middleware/utils/scm_app_http.py b/rhodecode/lib/middleware/utils/scm_app_http.py --- a/rhodecode/lib/middleware/utils/scm_app_http.py +++ b/rhodecode/lib/middleware/utils/scm_app_http.py @@ -167,7 +167,8 @@ def _is_request_chunked(environ): def _maybe_stream_request(environ): path = get_path_info(environ) stream = _is_request_chunked(environ) - log.debug('handling request `%s` with stream support: %s', path, stream) + req_method = environ['REQUEST_METHOD'] + log.debug('handling scm request: %s `%s` with stream support: %s', req_method, path, stream) if stream: # set stream by 256k diff --git a/rhodecode/lib/system_info.py b/rhodecode/lib/system_info.py --- a/rhodecode/lib/system_info.py +++ b/rhodecode/lib/system_info.py @@ -665,6 +665,32 @@ def vcs_server_config(): return SysInfoRes(value=value, state=state, human_value=human_value) +@register_sysinfo +def rhodecode_server_config(): + import rhodecode + + state = STATE_OK_DEFAULT + config = rhodecode.CONFIG.copy() + + secrets_lits = [ + f'rhodecode_{LicenseModel.LICENSE_DB_KEY}', + 'sqlalchemy.db1.url', + 'channelstream.secret', + 'beaker.session.secret', + 'rhodecode.encrypted_values.secret', + 'appenlight.api_key', + 'smtp_password', + 'file_store.objectstore.secret', + 'archive_cache.objectstore.secret', + 'app.service_api.token', + ] + for k in secrets_lits: + if k in config: + config[k] = '**OBFUSCATED**' + + value = human_value = config + return SysInfoRes(value=value, state=state, human_value=human_value) + @register_sysinfo def rhodecode_app_info(): @@ -851,6 +877,7 @@ def get_system_info(environ): 'vcs_server': SysInfo(vcs_server)(), 'vcs_server_config': SysInfo(vcs_server_config)(), + 'rhodecode_server_config': SysInfo(rhodecode_server_config)(), 'git': SysInfo(git_info)(), 'hg': SysInfo(hg_info)(), diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -452,15 +452,24 @@ class RepoModel(BaseModel): setattr(cur_repo, k, val) - new_name = cur_repo.get_new_name(kwargs['repo_name']) - cur_repo.repo_name = new_name + new_name = source_repo_name + if 'repo_name' in kwargs: + new_name = cur_repo.get_new_name(kwargs['repo_name']) + cur_repo.repo_name = new_name - # if private flag is set, reset default permission to NONE - if kwargs.get('repo_private'): - EMPTY_PERM = 'repository.none' - RepoModel().grant_user_permission( - repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM - ) + if 'repo_private' in kwargs: + # if private flag is set to True, reset default permission to NONE + set_private_to = kwargs.get('repo_private') + if set_private_to: + EMPTY_PERM = 'repository.none' + RepoModel().grant_user_permission( + repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM + ) + if set_private_to != cur_repo.private: + # NOTE(dan): we change repo private mode we need to notify all USERS + # this is just by having this value set to a different value then it was before + affected_user_ids = User.get_all_user_ids() + if kwargs.get('repo_landing_rev'): landing_rev_val = kwargs['repo_landing_rev'] RepoModel().set_landing_rev(cur_repo, landing_rev_val) diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2023 RhodeCode GmbH +# Copyright (C) 2011-2024 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 @@ -30,7 +30,6 @@ import time import traceback import string -from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode import events from rhodecode.model import BaseModel @@ -38,7 +37,7 @@ from rhodecode.model.db import (_hash_ke Session, RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm, UserGroup, Repository) from rhodecode.model.permission import PermissionModel -from rhodecode.model.settings import VcsSettingsModel, SettingsModel +from rhodecode.model.settings import SettingsModel from rhodecode.lib.caching_query import FromCache from rhodecode.lib.utils2 import action_logger_generic @@ -350,46 +349,45 @@ class RepoGroupModel(BaseModel): 'default_user_changed': None } - def _set_perm_user(obj, user, perm): - if isinstance(obj, RepoGroup): - self.grant_user_permission( - repo_group=obj, user=user, perm=perm) - elif isinstance(obj, Repository): + def _set_perm_user(_obj: RepoGroup | Repository, _user_obj: User, _perm): + + if isinstance(_obj, RepoGroup): + self.grant_user_permission(repo_group=_obj, user=_user_obj, perm=_perm) + elif isinstance(_obj, Repository): # private repos will not allow to change the default # permissions using recursive mode - if obj.private and user == User.DEFAULT_USER: + if _obj.private and _user_obj.username == User.DEFAULT_USER: + log.debug('Skipping private repo %s for user %s', _obj, _user_obj) return - # we set group permission but we have to switch to repo - # permission - perm = perm.replace('group.', 'repository.') - RepoModel().grant_user_permission( - repo=obj, user=user, perm=perm) + # we set group permission, we have to switch to repo permission definition + new_perm = _perm.replace('group.', 'repository.') + RepoModel().grant_user_permission(repo=_obj, user=_user_obj, perm=new_perm) + + def _set_perm_group(_obj: RepoGroup | Repository, users_group: UserGroup, _perm): + if isinstance(_obj, RepoGroup): + self.grant_user_group_permission(repo_group=_obj, group_name=users_group, perm=_perm) + elif isinstance(_obj, Repository): + # we set group permission, we have to switch to repo permission definition + new_perm = _perm.replace('group.', 'repository.') + RepoModel().grant_user_group_permission(repo=_obj, group_name=users_group, perm=new_perm) - def _set_perm_group(obj, users_group, perm): - if isinstance(obj, RepoGroup): - self.grant_user_group_permission( - repo_group=obj, group_name=users_group, perm=perm) - elif isinstance(obj, Repository): - # we set group permission but we have to switch to repo - # permission - perm = perm.replace('group.', 'repository.') - RepoModel().grant_user_group_permission( - repo=obj, group_name=users_group, perm=perm) + def _revoke_perm_user(_obj: RepoGroup | Repository, _user_obj: User): + if isinstance(_obj, RepoGroup): + self.revoke_user_permission(repo_group=_obj, user=_user_obj) + elif isinstance(_obj, Repository): + # private repos will not allow to change the default + # permissions using recursive mode, also there's no revocation fo default user, just update + if _user_obj.username == User.DEFAULT_USER: + log.debug('Skipping private repo %s for user %s', _obj, _user_obj) + return + RepoModel().revoke_user_permission(repo=_obj, user=_user_obj) - def _revoke_perm_user(obj, user): - if isinstance(obj, RepoGroup): - self.revoke_user_permission(repo_group=obj, user=user) - elif isinstance(obj, Repository): - RepoModel().revoke_user_permission(repo=obj, user=user) - - def _revoke_perm_group(obj, user_group): - if isinstance(obj, RepoGroup): - self.revoke_user_group_permission( - repo_group=obj, group_name=user_group) - elif isinstance(obj, Repository): - RepoModel().revoke_user_group_permission( - repo=obj, group_name=user_group) + def _revoke_perm_group(_obj: RepoGroup | Repository, user_group: UserGroup): + if isinstance(_obj, RepoGroup): + self.revoke_user_group_permission(repo_group=_obj, group_name=user_group) + elif isinstance(_obj, Repository): + RepoModel().revoke_user_group_permission(repo=_obj, group_name=user_group) # start updates log.debug('Now updating permissions for %s in recursive mode:%s', @@ -423,7 +421,8 @@ class RepoGroupModel(BaseModel): for member_id, perm, member_type in perm_updates: member_id = int(member_id) if member_type == 'user': - member_name = User.get(member_id).username + member_obj = User.get(member_id) + member_name = member_obj.username if isinstance(obj, RepoGroup) and obj == repo_group and member_name == User.DEFAULT_USER: # NOTE(dan): detect if we changed permissions for default user perm_obj = self.sa.query(UserRepoGroupToPerm) \ @@ -434,15 +433,16 @@ class RepoGroupModel(BaseModel): changes['default_user_changed'] = True # this updates also current one if found - _set_perm_user(obj, user=member_id, perm=perm) + _set_perm_user(obj, member_obj, perm) elif member_type == 'user_group': - member_name = UserGroup.get(member_id).users_group_name - if not check_perms or has_group_perm(member_name, - user=cur_user): - _set_perm_group(obj, users_group=member_id, perm=perm) + member_obj = UserGroup.get(member_id) + member_name = member_obj.users_group_name + if not check_perms or has_group_perm(member_name, user=cur_user): + _set_perm_group(obj, member_obj, perm) else: - raise ValueError("member_type must be 'user' or 'user_group' " - "got {} instead".format(member_type)) + raise ValueError( + f"member_type must be 'user' or 'user_group' got {member_type} instead" + ) changes['updated'].append( {'change_obj': change_obj, 'type': member_type, @@ -452,17 +452,19 @@ class RepoGroupModel(BaseModel): for member_id, perm, member_type in perm_additions: member_id = int(member_id) if member_type == 'user': - member_name = User.get(member_id).username - _set_perm_user(obj, user=member_id, perm=perm) + member_obj = User.get(member_id) + member_name = member_obj.username + _set_perm_user(obj, member_obj, perm) elif member_type == 'user_group': # check if we have permissions to alter this usergroup - member_name = UserGroup.get(member_id).users_group_name - if not check_perms or has_group_perm(member_name, - user=cur_user): - _set_perm_group(obj, users_group=member_id, perm=perm) + member_obj = UserGroup.get(member_id) + member_name = member_obj.users_group_name + if not check_perms or has_group_perm(member_name, user=cur_user): + _set_perm_group(obj, member_obj, perm) else: - raise ValueError("member_type must be 'user' or 'user_group' " - "got {} instead".format(member_type)) + raise ValueError( + f"member_type must be 'user' or 'user_group' got {member_type} instead" + ) changes['added'].append( {'change_obj': change_obj, 'type': member_type, @@ -472,18 +474,19 @@ class RepoGroupModel(BaseModel): for member_id, perm, member_type in perm_deletions: member_id = int(member_id) if member_type == 'user': - member_name = User.get(member_id).username - _revoke_perm_user(obj, user=member_id) + member_obj = User.get(member_id) + member_name = member_obj.username + _revoke_perm_user(obj, member_obj) elif member_type == 'user_group': # check if we have permissions to alter this usergroup - member_name = UserGroup.get(member_id).users_group_name - if not check_perms or has_group_perm(member_name, - user=cur_user): - _revoke_perm_group(obj, user_group=member_id) + member_obj = UserGroup.get(member_id) + member_name = member_obj.users_group_name + if not check_perms or has_group_perm(member_name, user=cur_user): + _revoke_perm_group(obj, member_obj) else: - raise ValueError("member_type must be 'user' or 'user_group' " - "got {} instead".format(member_type)) - + raise ValueError( + f"member_type must be 'user' or 'user_group' got {member_type} instead" + ) changes['deleted'].append( {'change_obj': change_obj, 'type': member_type, 'id': member_id, 'name': member_name, 'new_perm': perm}) diff --git a/rhodecode/model/update.py b/rhodecode/model/update.py --- a/rhodecode/model/update.py +++ b/rhodecode/model/update.py @@ -42,7 +42,7 @@ class UpdateModel(BaseModel): ver = rhodecode.__version__ log.debug('Checking for upgrade on `%s` server', update_url) opener = urllib.request.build_opener() - opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)] + opener.addheaders = [('User-agent', f'RhodeCode-SCM/{ver.strip()}')] response = opener.open(update_url) response_data = response.read() data = json.loads(response_data) diff --git a/rhodecode/templates/admin/settings/settings_system.mako b/rhodecode/templates/admin/settings/settings_system.mako --- a/rhodecode/templates/admin/settings/settings_system.mako +++ b/rhodecode/templates/admin/settings/settings_system.mako @@ -29,7 +29,21 @@
-

${_('VCS Server')}

+

${_('RhodeCode Server Config')}

+
+
+
+ % for dt, dd in c.rhodecode_data_items: +
${dt}${':' if dt else '---'}
+
${dd}${'' if dt else '---'}
+ % endfor +
+
+
+ +
+
+

${_('VCS Server Config')}

diff --git a/rhodecode/tests/models/test_permissions.py b/rhodecode/tests/models/test_permissions.py --- a/rhodecode/tests/models/test_permissions.py +++ b/rhodecode/tests/models/test_permissions.py @@ -166,6 +166,74 @@ class TestPermissions(object): test_user_group.user = org_owner + def test_propagated_permissions_from_repo_group_to_private_repo(self, repo_name): + # make group + self.g1 = fixture.create_repo_group('TOP_LEVEL', skip_if_exists=True) + # both perms should be read ! + assert group_perms(self.anon) == { + 'TOP_LEVEL': 'group.read' + } + + # Create repo inside the TOP_LEVEL + repo_name_in_group = RepoGroup.url_sep().join([self.g1.group_name, 'test_perm_on_private_repo']) + self.test_repo = fixture.create_repo(name=repo_name_in_group, + repo_type='hg', + repo_group=self.g1, + cur_user=self.u1,) + assert repo_perms(self.anon) == { + repo_name_in_group: 'repository.read', + 'vcs_test_git': 'repository.read', + 'vcs_test_hg': 'repository.read', + 'vcs_test_svn': 'repository.read', + } + # Now change default user permissions + new_perm = 'repository.write' + perm_updates = [ + [self.anon.user_id, new_perm, 'user'] + ] + RepoGroupModel().update_permissions( + repo_group=self.g1, perm_updates=perm_updates, recursive='all') + + Session().commit() + assert repo_perms(self.anon) == { + repo_name_in_group: new_perm, + 'vcs_test_git': 'repository.read', + 'vcs_test_hg': 'repository.read', + 'vcs_test_svn': 'repository.read', + } + + # NOW MARK repo as private + changes = { + 'repo_private': True + } + repo = RepoModel().get_by_repo_name(repo_name_in_group) + RepoModel().update(repo, **changes) + Session().commit() + + # Private repo sets 'none' permission for default user + assert repo_perms(self.anon) == { + repo_name_in_group: 'repository.none', + 'vcs_test_git': 'repository.read', + 'vcs_test_hg': 'repository.read', + 'vcs_test_svn': 'repository.read', + } + + # apply same logic of "updated" recursive, but now the anon permissions should be not be impacted + new_perm = 'repository.write' + perm_updates = [ + [self.anon.user_id, new_perm, 'user'] + ] + RepoGroupModel().update_permissions( + repo_group=self.g1, perm_updates=perm_updates, recursive='all') + + Session().commit() + assert repo_perms(self.anon) == { + repo_name_in_group: 'repository.none', + 'vcs_test_git': 'repository.read', + 'vcs_test_hg': 'repository.read', + 'vcs_test_svn': 'repository.read', + } + def test_propagated_permission_from_users_group_by_explicit_perms_exist( self, repo_name): # make group diff --git a/rhodecode/tests/rhodecode.ini b/rhodecode/tests/rhodecode.ini --- a/rhodecode/tests/rhodecode.ini +++ b/rhodecode/tests/rhodecode.ini @@ -71,6 +71,9 @@ use = egg:rhodecode-enterprise-ce ; enable proxy prefix middleware, defined above #filter-with = proxy-prefix +; control if environmental variables to be expanded into the .ini settings +rhodecode.env_expand = false + ; encryption key used to encrypt social plugin tokens, ; remote_urls with credentials etc, if not set it defaults to ; `beaker.session.secret` @@ -225,6 +228,13 @@ license_token = abra-cada-bra1-rce3 ; This flag hides sensitive information on the license page such as token, and license data license.hide_license_info = false +; Import EE license from this license path +#license.import_path = %(here)s/rhodecode_enterprise.license + +; import license 'if-missing' or 'force' (always override) +; if-missing means apply license if it doesn't exist. 'force' option always overrides it +license.import_path_mode = if-missing + ; supervisor connection uri, for managing supervisor and logs. supervisor.uri = @@ -658,6 +668,12 @@ vcs.connection_timeout = 3600 ; It uses cache_region `cache_repo` vcs.methods.cache = false +; Filesystem location where Git lfs objects should be stored +vcs.git.lfs.storage_location = /var/opt/rhodecode_repo_store/.cache/git_lfs_store + +; Filesystem location where Mercurial largefile objects should be stored +vcs.hg.largefiles.storage_location = /var/opt/rhodecode_repo_store/.cache/hg_largefiles_store + ; #################################################### ; Subversion proxy support (mod_dav_svn) ; Maps RhodeCode repo groups into SVN paths for Apache