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