##// END OF EJS Templates
feat(ssh-wrapper): added pre/post pull hooks on top of git for ssh backend....
super-admin -
r5302:399d1dbe default
parent child Browse files
Show More
@@ -1,160 +1,161 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import logging
22 22
23 23 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
24 24 from rhodecode.lib.ext_json import sjson as json
25 25 from rhodecode.lib.vcs.conf import settings as vcs_settings
26 26 from rhodecode.model.scm import ScmModel
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class VcsServer(object):
32 32 repo_user_agent = None # set in child classes
33 33 _path = None # set executable path for hg/git/svn binary
34 34 backend = None # set in child classes
35 35 tunnel = None # subprocess handling tunnel
36 36 write_perms = ['repository.admin', 'repository.write']
37 37 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38 38
39 39 def __init__(self, user, user_permissions, config, env):
40 40 self.user = user
41 41 self.user_permissions = user_permissions
42 42 self.config = config
43 43 self.env = env
44 44 self.stdin = sys.stdin
45 45
46 46 self.repo_name = None
47 47 self.repo_mode = None
48 48 self.store = ''
49 49 self.ini_path = ''
50 50
51 51 def _invalidate_cache(self, repo_name):
52 52 """
53 53 Set's cache for this repository for invalidation on next access
54 54
55 55 :param repo_name: full repo name, also a cache key
56 56 """
57 57 ScmModel().mark_for_invalidation(repo_name)
58 58
59 59 def has_write_perm(self):
60 60 permission = self.user_permissions.get(self.repo_name)
61 61 if permission in ['repository.write', 'repository.admin']:
62 62 return True
63 63
64 64 return False
65 65
66 66 def _check_permissions(self, action):
67 67 permission = self.user_permissions.get(self.repo_name)
68 68 log.debug('permission for %s on %s are: %s',
69 69 self.user, self.repo_name, permission)
70 70
71 71 if not permission:
72 72 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
73 73 self.user, self.repo_name)
74 74 return -2
75 75
76 76 if action == 'pull':
77 77 if permission in self.read_perms:
78 78 log.info(
79 79 'READ Permissions for User "%s" detected to repo "%s"!',
80 80 self.user, self.repo_name)
81 81 return 0
82 82 else:
83 83 if permission in self.write_perms:
84 84 log.info(
85 85 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
86 86 self.user, self.repo_name)
87 87 return 0
88 88
89 89 log.error('Cannot properly fetch or verify user `%s` permissions. '
90 90 'Permissions: %s, vcs action: %s',
91 91 self.user, permission, action)
92 92 return -2
93 93
94 94 def update_environment(self, action, extras=None):
95 95
96 96 scm_data = {
97 97 'ip': os.environ['SSH_CLIENT'].split()[0],
98 98 'username': self.user.username,
99 99 'user_id': self.user.user_id,
100 100 'action': action,
101 101 'repository': self.repo_name,
102 102 'scm': self.backend,
103 103 'config': self.ini_path,
104 104 'repo_store': self.store,
105 105 'make_lock': None,
106 106 'locked_by': [None, None],
107 107 'server_url': None,
108 108 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
109 109 'hooks': ['push', 'pull'],
110 110 'hooks_module': 'rhodecode.lib.hooks_daemon',
111 111 'is_shadow_repo': False,
112 112 'detect_force_push': False,
113 113 'check_branch_perms': False,
114 114
115 115 'SSH': True,
116 116 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
117 117 }
118 118 if extras:
119 119 scm_data.update(extras)
120 120 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
121 return scm_data
121 122
122 123 def get_root_store(self):
123 124 root_store = self.store
124 125 if not root_store.endswith('/'):
125 126 # always append trailing slash
126 127 root_store = root_store + '/'
127 128 return root_store
128 129
129 130 def _handle_tunnel(self, extras):
130 131 # pre-auth
131 132 action = 'pull'
132 133 exit_code = self._check_permissions(action)
133 134 if exit_code:
134 135 return exit_code, False
135 136
136 137 req = self.env['request']
137 138 server_url = req.host_url + req.script_name
138 139 extras['server_url'] = server_url
139 140
140 141 log.debug('Using %s binaries from path %s', self.backend, self._path)
141 142 exit_code = self.tunnel.run(extras)
142 143
143 144 return exit_code, action == "push"
144 145
145 146 def run(self, tunnel_extras=None):
146 147 tunnel_extras = tunnel_extras or {}
147 148 extras = {}
148 149 extras.update(tunnel_extras)
149 150
150 151 callback_daemon, extras = prepare_callback_daemon(
151 152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
152 153 host=vcs_settings.HOOKS_HOST)
153 154
154 155 with callback_daemon:
155 156 try:
156 157 return self._handle_tunnel(extras)
157 158 finally:
158 159 log.debug('Running cleanup with cache invalidation')
159 160 if self.repo_name:
160 161 self._invalidate_cache(self.repo_name)
@@ -1,73 +1,87 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 import os
20 19 import sys
21 20 import logging
21 import subprocess
22 22
23 from vcsserver import hooks
23 24 from .base import VcsServer
24 25
25 26 log = logging.getLogger(__name__)
26 27
27 28
28 29 class GitTunnelWrapper(object):
29 30 process = None
30 31
31 32 def __init__(self, server):
32 33 self.server = server
33 34 self.stdin = sys.stdin
34 35 self.stdout = sys.stdout
35 36
36 37 def create_hooks_env(self):
37 38 pass
38 39
39 40 def command(self):
40 41 root = self.server.get_root_store()
41 42 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
42 43 root=root, git_path=self.server.git_path,
43 44 mode=self.server.repo_mode, repo_name=self.server.repo_name)
44 45 log.debug("Final CMD: %s", command)
45 46 return command
46 47
47 48 def run(self, extras):
48 49 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
49 50 exit_code = self.server._check_permissions(action)
50 51 if exit_code:
51 52 return exit_code
52 53
53 self.server.update_environment(action=action, extras=extras)
54 scm_extras = self.server.update_environment(action=action, extras=extras)
55
56 hook_response = hooks.git_pre_pull(scm_extras)
57 pre_pull_messages = hook_response.output
58 sys.stdout.write(pre_pull_messages)
59
54 60 self.create_hooks_env()
55 return os.system(self.command())
61 result = subprocess.run(self.command(), shell=True)
62 result = result.returncode
63
64 # Upload-pack == clone
65 if action == "pull":
66 hook_response = hooks.git_post_pull(scm_extras)
67 post_pull_messages = hook_response.output
68 sys.stderr.write(post_pull_messages)
69 return result
56 70
57 71
58 72 class GitServer(VcsServer):
59 73 backend = 'git'
60 74 repo_user_agent = 'git'
61 75
62 76 def __init__(self, store, ini_path, repo_name, repo_mode,
63 77 user, user_permissions, config, env):
64 78 super().\
65 79 __init__(user, user_permissions, config, env)
66 80
67 81 self.store = store
68 82 self.ini_path = ini_path
69 83 self.repo_name = repo_name
70 84 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
71 85
72 86 self.repo_mode = repo_mode
73 87 self.tunnel = GitTunnelWrapper(server=self)
General Comments 0
You need to be logged in to leave comments. Login now