# Copyright (C) 2016-2023 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 # (only), as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ import os import sys import logging from rhodecode.lib.hooks_daemon import prepare_callback_daemon from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.vcs.conf import settings as vcs_settings from rhodecode.model.scm import ScmModel log = logging.getLogger(__name__) class VcsServer(object): repo_user_agent = None # set in child classes _path = None # set executable path for hg/git/svn binary backend = None # set in child classes tunnel = None # subprocess handling tunnel write_perms = ['repository.admin', 'repository.write'] read_perms = ['repository.read', 'repository.admin', 'repository.write'] def __init__(self, user, user_permissions, config, env): self.user = user self.user_permissions = user_permissions self.config = config self.env = env self.stdin = sys.stdin self.repo_name = None self.repo_mode = None self.store = '' self.ini_path = '' def _invalidate_cache(self, repo_name): """ Set's cache for this repository for invalidation on next access :param repo_name: full repo name, also a cache key """ ScmModel().mark_for_invalidation(repo_name) def has_write_perm(self): permission = self.user_permissions.get(self.repo_name) if permission in ['repository.write', 'repository.admin']: return True return False def _check_permissions(self, action): permission = self.user_permissions.get(self.repo_name) log.debug('permission for %s on %s are: %s', self.user, self.repo_name, permission) if not permission: log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', self.user, self.repo_name) return -2 if action == 'pull': if permission in self.read_perms: log.info( 'READ Permissions for User "%s" detected to repo "%s"!', self.user, self.repo_name) return 0 else: if permission in self.write_perms: log.info( 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!', self.user, self.repo_name) return 0 log.error('Cannot properly fetch or verify user `%s` permissions. ' 'Permissions: %s, vcs action: %s', self.user, permission, action) return -2 def update_environment(self, action, extras=None): scm_data = { 'ip': os.environ['SSH_CLIENT'].split()[0], 'username': self.user.username, 'user_id': self.user.user_id, 'action': action, 'repository': self.repo_name, 'scm': self.backend, 'config': self.ini_path, 'repo_store': self.store, 'make_lock': None, 'locked_by': [None, None], 'server_url': None, 'user_agent': f'{self.repo_user_agent}/ssh-user-agent', 'hooks': ['push', 'pull'], 'hooks_module': 'rhodecode.lib.hooks_daemon', 'is_shadow_repo': False, 'detect_force_push': False, 'check_branch_perms': False, 'SSH': True, 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name), } if extras: scm_data.update(extras) os.putenv("RC_SCM_DATA", json.dumps(scm_data)) return scm_data def get_root_store(self): root_store = self.store if not root_store.endswith('/'): # always append trailing slash root_store = root_store + '/' return root_store def _handle_tunnel(self, extras): # pre-auth action = 'pull' exit_code = self._check_permissions(action) if exit_code: return exit_code, False req = self.env['request'] server_url = req.host_url + req.script_name extras['server_url'] = server_url log.debug('Using %s binaries from path %s', self.backend, self._path) exit_code = self.tunnel.run(extras) return exit_code, action == "push" def run(self, tunnel_extras=None): tunnel_extras = tunnel_extras or {} extras = {} extras.update(tunnel_extras) callback_daemon, extras = prepare_callback_daemon( extras, protocol=vcs_settings.HOOKS_PROTOCOL, host=vcs_settings.HOOKS_HOST) with callback_daemon: try: return self._handle_tunnel(extras) finally: log.debug('Running cleanup with cache invalidation') if self.repo_name: self._invalidate_cache(self.repo_name)