##// END OF EJS Templates
ssh: use pre-compiled backends for faster matching of vcs detection.
super-admin -
r4702:c3d9862a stable
parent child Browse files
Show More
@@ -1,228 +1,227 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import re
22 import re
23 import logging
23 import logging
24 import datetime
24 import datetime
25 from pyramid.compat import configparser
25 from pyramid.compat import configparser
26
26
27 from rhodecode.model.db import Session, User, UserSshKeys
27 from rhodecode.model.db import Session, User, UserSshKeys
28 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
29
29
30 from .hg import MercurialServer
30 from .hg import MercurialServer
31 from .git import GitServer
31 from .git import GitServer
32 from .svn import SubversionServer
32 from .svn import SubversionServer
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class SshWrapper(object):
36 class SshWrapper(object):
37 hg_cmd_pat = re.compile(r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$')
38 git_cmd_pat = re.compile(r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
39 svn_cmd_pat = re.compile(r'^svnserve -t')
37
40
38 def __init__(self, command, connection_info, mode,
41 def __init__(self, command, connection_info, mode,
39 user, user_id, key_id, shell, ini_path, env):
42 user, user_id, key_id, shell, ini_path, env):
40 self.command = command
43 self.command = command
41 self.connection_info = connection_info
44 self.connection_info = connection_info
42 self.mode = mode
45 self.mode = mode
43 self.user = user
46 self.user = user
44 self.user_id = user_id
47 self.user_id = user_id
45 self.key_id = key_id
48 self.key_id = key_id
46 self.shell = shell
49 self.shell = shell
47 self.ini_path = ini_path
50 self.ini_path = ini_path
48 self.env = env
51 self.env = env
49
52
50 self.config = self.parse_config(ini_path)
53 self.config = self.parse_config(ini_path)
51 self.server_impl = None
54 self.server_impl = None
52
55
53 def parse_config(self, config_path):
56 def parse_config(self, config_path):
54 parser = configparser.ConfigParser()
57 parser = configparser.ConfigParser()
55 parser.read(config_path)
58 parser.read(config_path)
56 return parser
59 return parser
57
60
58 def update_key_access_time(self, key_id):
61 def update_key_access_time(self, key_id):
59 key = UserSshKeys().query().filter(
62 key = UserSshKeys().query().filter(
60 UserSshKeys.ssh_key_id == key_id).scalar()
63 UserSshKeys.ssh_key_id == key_id).scalar()
61 if key:
64 if key:
62 key.accessed_on = datetime.datetime.utcnow()
65 key.accessed_on = datetime.datetime.utcnow()
63 Session().add(key)
66 Session().add(key)
64 Session().commit()
67 Session().commit()
65 log.debug('Update key id:`%s` fingerprint:`%s` access time',
68 log.debug('Update key id:`%s` fingerprint:`%s` access time',
66 key_id, key.ssh_key_fingerprint)
69 key_id, key.ssh_key_fingerprint)
67
70
68 def get_connection_info(self):
71 def get_connection_info(self):
69 """
72 """
70 connection_info
73 connection_info
71
74
72 Identifies the client and server ends of the connection.
75 Identifies the client and server ends of the connection.
73 The variable contains four space-separated values: client IP address,
76 The variable contains four space-separated values: client IP address,
74 client port number, server IP address, and server port number.
77 client port number, server IP address, and server port number.
75 """
78 """
76 conn = dict(
79 conn = dict(
77 client_ip=None,
80 client_ip=None,
78 client_port=None,
81 client_port=None,
79 server_ip=None,
82 server_ip=None,
80 server_port=None,
83 server_port=None,
81 )
84 )
82
85
83 info = self.connection_info.split(' ')
86 info = self.connection_info.split(' ')
84 if len(info) == 4:
87 if len(info) == 4:
85 conn['client_ip'] = info[0]
88 conn['client_ip'] = info[0]
86 conn['client_port'] = info[1]
89 conn['client_port'] = info[1]
87 conn['server_ip'] = info[2]
90 conn['server_ip'] = info[2]
88 conn['server_port'] = info[3]
91 conn['server_port'] = info[3]
89
92
90 return conn
93 return conn
91
94
92 def maybe_translate_repo_uid(self, repo_name):
95 def maybe_translate_repo_uid(self, repo_name):
93 if repo_name.startswith('_'):
96 if repo_name.startswith('_'):
94 from rhodecode.model.repo import RepoModel
97 from rhodecode.model.repo import RepoModel
95 by_id_match = RepoModel().get_repo_by_id(repo_name)
98 by_id_match = RepoModel().get_repo_by_id(repo_name)
96 if by_id_match:
99 if by_id_match:
97 repo_name = by_id_match.repo_name
100 repo_name = by_id_match.repo_name
98 return repo_name
101 return repo_name
99
102
100 def get_repo_details(self, mode):
103 def get_repo_details(self, mode):
101 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
104 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
102 repo_name = None
105 repo_name = None
103
106
104 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
107 hg_match = self.hg_cmd_pat.match(self.command)
105 hg_match = re.match(hg_pattern, self.command)
106 if hg_match is not None:
108 if hg_match is not None:
107 vcs_type = 'hg'
109 vcs_type = 'hg'
108 repo_name = self.maybe_translate_repo_uid(hg_match.group(1).strip('/'))
110 repo_name = self.maybe_translate_repo_uid(hg_match.group(1).strip('/'))
109 return vcs_type, repo_name, mode
111 return vcs_type, repo_name, mode
110
112
111 git_pattern = r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$'
113 git_match = self.git_cmd_pat.match(self.command)
112 git_match = re.match(git_pattern, self.command)
113 if git_match is not None:
114 if git_match is not None:
114 vcs_type = 'git'
115 vcs_type = 'git'
115 repo_name = self.maybe_translate_repo_uid(git_match.group(2).strip('/'))
116 repo_name = self.maybe_translate_repo_uid(git_match.group(2).strip('/'))
116 mode = git_match.group(1)
117 mode = git_match.group(1)
117 return vcs_type, repo_name, mode
118 return vcs_type, repo_name, mode
118
119
119 svn_pattern = r'^svnserve -t'
120 svn_match = self.svn_cmd_pat.match(self.command)
120 svn_match = re.match(svn_pattern, self.command)
121
122 if svn_match is not None:
121 if svn_match is not None:
123 vcs_type = 'svn'
122 vcs_type = 'svn'
124 # Repo name should be extracted from the input stream, we're unable to
123 # Repo name should be extracted from the input stream, we're unable to
125 # extract it at this point in execution
124 # extract it at this point in execution
126 return vcs_type, repo_name, mode
125 return vcs_type, repo_name, mode
127
126
128 return vcs_type, repo_name, mode
127 return vcs_type, repo_name, mode
129
128
130 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
129 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
131 store = ScmModel().repos_path
130 store = ScmModel().repos_path
132
131
133 check_branch_perms = False
132 check_branch_perms = False
134 detect_force_push = False
133 detect_force_push = False
135
134
136 if branch_permissions:
135 if branch_permissions:
137 check_branch_perms = True
136 check_branch_perms = True
138 detect_force_push = True
137 detect_force_push = True
139
138
140 log.debug(
139 log.debug(
141 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
140 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
142 vcs, mode, repo, check_branch_perms)
141 vcs, mode, repo, check_branch_perms)
143
142
144 # detect if we have to check branch permissions
143 # detect if we have to check branch permissions
145 extras = {
144 extras = {
146 'detect_force_push': detect_force_push,
145 'detect_force_push': detect_force_push,
147 'check_branch_perms': check_branch_perms,
146 'check_branch_perms': check_branch_perms,
148 }
147 }
149
148
150 if vcs == 'hg':
149 if vcs == 'hg':
151 server = MercurialServer(
150 server = MercurialServer(
152 store=store, ini_path=self.ini_path,
151 store=store, ini_path=self.ini_path,
153 repo_name=repo, user=user,
152 repo_name=repo, user=user,
154 user_permissions=permissions, config=self.config, env=self.env)
153 user_permissions=permissions, config=self.config, env=self.env)
155 self.server_impl = server
154 self.server_impl = server
156 return server.run(tunnel_extras=extras)
155 return server.run(tunnel_extras=extras)
157
156
158 elif vcs == 'git':
157 elif vcs == 'git':
159 server = GitServer(
158 server = GitServer(
160 store=store, ini_path=self.ini_path,
159 store=store, ini_path=self.ini_path,
161 repo_name=repo, repo_mode=mode, user=user,
160 repo_name=repo, repo_mode=mode, user=user,
162 user_permissions=permissions, config=self.config, env=self.env)
161 user_permissions=permissions, config=self.config, env=self.env)
163 self.server_impl = server
162 self.server_impl = server
164 return server.run(tunnel_extras=extras)
163 return server.run(tunnel_extras=extras)
165
164
166 elif vcs == 'svn':
165 elif vcs == 'svn':
167 server = SubversionServer(
166 server = SubversionServer(
168 store=store, ini_path=self.ini_path,
167 store=store, ini_path=self.ini_path,
169 repo_name=None, user=user,
168 repo_name=None, user=user,
170 user_permissions=permissions, config=self.config, env=self.env)
169 user_permissions=permissions, config=self.config, env=self.env)
171 self.server_impl = server
170 self.server_impl = server
172 return server.run(tunnel_extras=extras)
171 return server.run(tunnel_extras=extras)
173
172
174 else:
173 else:
175 raise Exception('Unrecognised VCS: {}'.format(vcs))
174 raise Exception('Unrecognised VCS: {}'.format(vcs))
176
175
177 def wrap(self):
176 def wrap(self):
178 mode = self.mode
177 mode = self.mode
179 user = self.user
178 user = self.user
180 user_id = self.user_id
179 user_id = self.user_id
181 key_id = self.key_id
180 key_id = self.key_id
182 shell = self.shell
181 shell = self.shell
183
182
184 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
183 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
185
184
186 log.debug(
185 log.debug(
187 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
186 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
188 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
187 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
189 mode, user, user_id, shell, self.command,
188 mode, user, user_id, shell, self.command,
190 scm_detected, scm_mode, scm_repo)
189 scm_detected, scm_mode, scm_repo)
191
190
192 # update last access time for this key
191 # update last access time for this key
193 self.update_key_access_time(key_id)
192 self.update_key_access_time(key_id)
194
193
195 log.debug('SSH Connection info %s', self.get_connection_info())
194 log.debug('SSH Connection info %s', self.get_connection_info())
196
195
197 if shell and self.command is None:
196 if shell and self.command is None:
198 log.info('Dropping to shell, no command given and shell is allowed')
197 log.info('Dropping to shell, no command given and shell is allowed')
199 os.execl('/bin/bash', '-l')
198 os.execl('/bin/bash', '-l')
200 exit_code = 1
199 exit_code = 1
201
200
202 elif scm_detected:
201 elif scm_detected:
203 user = User.get(user_id)
202 user = User.get(user_id)
204 if not user:
203 if not user:
205 log.warning('User with id %s not found', user_id)
204 log.warning('User with id %s not found', user_id)
206 exit_code = -1
205 exit_code = -1
207 return exit_code
206 return exit_code
208
207
209 auth_user = user.AuthUser()
208 auth_user = user.AuthUser()
210 permissions = auth_user.permissions['repositories']
209 permissions = auth_user.permissions['repositories']
211 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
210 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
212 try:
211 try:
213 exit_code, is_updated = self.serve(
212 exit_code, is_updated = self.serve(
214 scm_detected, scm_repo, scm_mode, user, permissions,
213 scm_detected, scm_repo, scm_mode, user, permissions,
215 repo_branch_permissions)
214 repo_branch_permissions)
216 except Exception:
215 except Exception:
217 log.exception('Error occurred during execution of SshWrapper')
216 log.exception('Error occurred during execution of SshWrapper')
218 exit_code = -1
217 exit_code = -1
219
218
220 elif self.command is None and shell is False:
219 elif self.command is None and shell is False:
221 log.error('No Command given.')
220 log.error('No Command given.')
222 exit_code = -1
221 exit_code = -1
223
222
224 else:
223 else:
225 log.error('Unhandled Command: "%s" Aborting.', self.command)
224 log.error('Unhandled Command: "%s" Aborting.', self.command)
226 exit_code = -1
225 exit_code = -1
227
226
228 return exit_code
227 return exit_code
General Comments 0
You need to be logged in to leave comments. Login now