base.py
175 lines
| 6.2 KiB
| text/x-python
|
PythonLexer
r5088 | # Copyright (C) 2016-2023 RhodeCode GmbH | |||
r2187 | # | |||
# 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 <http://www.gnu.org/licenses/>. | ||||
# | ||||
# 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 | ||||
r5325 | from rhodecode.lib.hook_daemon.base import prepare_callback_daemon | |||
r5065 | from rhodecode.lib.ext_json import sjson as json | |||
r2212 | from rhodecode.lib.vcs.conf import settings as vcs_settings | |||
r5325 | from rhodecode.lib.api_utils import call_service_api | |||
r2187 | ||||
log = logging.getLogger(__name__) | ||||
r5326 | class SshVcsServer(object): | |||
r4858 | repo_user_agent = None # set in child classes | |||
r2187 | _path = None # set executable path for hg/git/svn binary | |||
backend = None # set in child classes | ||||
tunnel = None # subprocess handling tunnel | ||||
r5325 | settings = None # parsed settings module | |||
r2187 | write_perms = ['repository.admin', 'repository.write'] | |||
read_perms = ['repository.read', 'repository.admin', 'repository.write'] | ||||
r5325 | def __init__(self, user, user_permissions, settings, env): | |||
r2187 | self.user = user | |||
self.user_permissions = user_permissions | ||||
r5325 | self.settings = settings | |||
r2187 | self.env = env | |||
self.stdin = sys.stdin | ||||
self.repo_name = None | ||||
self.repo_mode = None | ||||
self.store = '' | ||||
self.ini_path = '' | ||||
r5314 | self.hooks_protocol = None | |||
r2187 | ||||
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 | ||||
""" | ||||
r5314 | # Todo: Leave only "celery" case after transition. | |||
match self.hooks_protocol: | ||||
case 'http': | ||||
r5325 | from rhodecode.model.scm import ScmModel | |||
r5314 | ScmModel().mark_for_invalidation(repo_name) | |||
case 'celery': | ||||
r5326 | call_service_api(self.settings, { | |||
r5314 | "method": "service_mark_for_invalidation", | |||
"args": {"repo_name": repo_name} | ||||
}) | ||||
r2187 | ||||
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) | ||||
r5314 | user_info = f'{self.user["user_id"]}:{self.user["username"]}' | |||
r4281 | log.debug('permission for %s on %s are: %s', | |||
r5314 | user_info, self.repo_name, permission) | |||
r2187 | ||||
r2973 | if not permission: | |||
log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', | ||||
r5314 | user_info, self.repo_name) | |||
r2973 | return -2 | |||
r2187 | if action == 'pull': | |||
if permission in self.read_perms: | ||||
log.info( | ||||
'READ Permissions for User "%s" detected to repo "%s"!', | ||||
r5314 | user_info, self.repo_name) | |||
r2187 | return 0 | |||
else: | ||||
if permission in self.write_perms: | ||||
log.info( | ||||
r4462 | 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!', | |||
r5314 | user_info, self.repo_name) | |||
r2187 | return 0 | |||
r2973 | log.error('Cannot properly fetch or verify user `%s` permissions. ' | |||
'Permissions: %s, vcs action: %s', | ||||
r5314 | user_info, permission, action) | |||
r2187 | return -2 | |||
def update_environment(self, action, extras=None): | ||||
scm_data = { | ||||
'ip': os.environ['SSH_CLIENT'].split()[0], | ||||
'username': self.user.username, | ||||
r2411 | 'user_id': self.user.user_id, | |||
r2187 | 'action': action, | |||
'repository': self.repo_name, | ||||
'scm': self.backend, | ||||
'config': self.ini_path, | ||||
r3094 | 'repo_store': self.store, | |||
r2187 | 'make_lock': None, | |||
'locked_by': [None, None], | ||||
'server_url': None, | ||||
r5093 | 'user_agent': f'{self.repo_user_agent}/ssh-user-agent', | |||
r2187 | 'hooks': ['push', 'pull'], | |||
r5325 | 'hooks_module': 'rhodecode.lib.hook_daemon.hook_module', | |||
r2982 | 'is_shadow_repo': False, | |||
'detect_force_push': False, | ||||
'check_branch_perms': False, | ||||
r2187 | 'SSH': True, | |||
r2982 | 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name), | |||
r2187 | } | |||
if extras: | ||||
scm_data.update(extras) | ||||
os.putenv("RC_SCM_DATA", json.dumps(scm_data)) | ||||
r5302 | return scm_data | |||
r2187 | ||||
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 | ||||
r5314 | req = self.env.get('request') | |||
if req: | ||||
server_url = req.host_url + req.script_name | ||||
extras['server_url'] = server_url | ||||
r2187 | ||||
log.debug('Using %s binaries from path %s', self.backend, self._path) | ||||
exit_code = self.tunnel.run(extras) | ||||
return exit_code, action == "push" | ||||
r2982 | def run(self, tunnel_extras=None): | |||
r5325 | self.hooks_protocol = self.settings['vcs.hooks.protocol'] | |||
r2982 | tunnel_extras = tunnel_extras or {} | |||
r2187 | extras = {} | |||
r2982 | extras.update(tunnel_extras) | |||
r2187 | ||||
callback_daemon, extras = prepare_callback_daemon( | ||||
r5314 | extras, protocol=self.hooks_protocol, | |||
r5298 | host=vcs_settings.HOOKS_HOST) | |||
r2187 | ||||
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) | ||||