##// END OF EJS Templates
ssh: prevent exceptions when user associated to stored old key is not found.
marcink -
r2206:68955e56 default
parent child Browse files
Show More
@@ -1,202 +1,207 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 import ConfigParser
25 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
37
38 def __init__(self, command, connection_info, mode,
38 def __init__(self, command, connection_info, mode,
39 user, user_id, key_id, shell, ini_path, env):
39 user, user_id, key_id, shell, ini_path, env):
40 self.command = command
40 self.command = command
41 self.connection_info = connection_info
41 self.connection_info = connection_info
42 self.mode = mode
42 self.mode = mode
43 self.user = user
43 self.user = user
44 self.user_id = user_id
44 self.user_id = user_id
45 self.key_id = key_id
45 self.key_id = key_id
46 self.shell = shell
46 self.shell = shell
47 self.ini_path = ini_path
47 self.ini_path = ini_path
48 self.env = env
48 self.env = env
49
49
50 self.config = self.parse_config(ini_path)
50 self.config = self.parse_config(ini_path)
51 self.server_impl = None
51 self.server_impl = None
52
52
53 def parse_config(self, config_path):
53 def parse_config(self, config_path):
54 parser = ConfigParser.ConfigParser()
54 parser = ConfigParser.ConfigParser()
55 parser.read(config_path)
55 parser.read(config_path)
56 return parser
56 return parser
57
57
58 def update_key_access_time(self, key_id):
58 def update_key_access_time(self, key_id):
59 key = UserSshKeys().query().filter(
59 key = UserSshKeys().query().filter(
60 UserSshKeys.ssh_key_id == key_id).scalar()
60 UserSshKeys.ssh_key_id == key_id).scalar()
61 if key:
61 if key:
62 key.accessed_on = datetime.datetime.utcnow()
62 key.accessed_on = datetime.datetime.utcnow()
63 Session().add(key)
63 Session().add(key)
64 Session().commit()
64 Session().commit()
65 log.debug('Update key `%s` access time', key_id)
65 log.debug('Update key `%s` access time', key_id)
66
66
67 def get_connection_info(self):
67 def get_connection_info(self):
68 """
68 """
69 connection_info
69 connection_info
70
70
71 Identifies the client and server ends of the connection.
71 Identifies the client and server ends of the connection.
72 The variable contains four space-separated values: client IP address,
72 The variable contains four space-separated values: client IP address,
73 client port number, server IP address, and server port number.
73 client port number, server IP address, and server port number.
74 """
74 """
75 conn = dict(
75 conn = dict(
76 client_ip=None,
76 client_ip=None,
77 client_port=None,
77 client_port=None,
78 server_ip=None,
78 server_ip=None,
79 server_port=None,
79 server_port=None,
80 )
80 )
81
81
82 info = self.connection_info.split(' ')
82 info = self.connection_info.split(' ')
83 if len(info) == 4:
83 if len(info) == 4:
84 conn['client_ip'] = info[0]
84 conn['client_ip'] = info[0]
85 conn['client_port'] = info[1]
85 conn['client_port'] = info[1]
86 conn['server_ip'] = info[2]
86 conn['server_ip'] = info[2]
87 conn['server_port'] = info[3]
87 conn['server_port'] = info[3]
88
88
89 return conn
89 return conn
90
90
91 def get_repo_details(self, mode):
91 def get_repo_details(self, mode):
92 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
92 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
93 mode = mode
93 mode = mode
94 repo_name = None
94 repo_name = None
95
95
96 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
96 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
97 hg_match = re.match(hg_pattern, self.command)
97 hg_match = re.match(hg_pattern, self.command)
98 if hg_match is not None:
98 if hg_match is not None:
99 vcs_type = 'hg'
99 vcs_type = 'hg'
100 repo_name = hg_match.group(1).strip('/')
100 repo_name = hg_match.group(1).strip('/')
101 return vcs_type, repo_name, mode
101 return vcs_type, repo_name, mode
102
102
103 git_pattern = (
103 git_pattern = (
104 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
104 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
105 git_match = re.match(git_pattern, self.command)
105 git_match = re.match(git_pattern, self.command)
106 if git_match is not None:
106 if git_match is not None:
107 vcs_type = 'git'
107 vcs_type = 'git'
108 repo_name = git_match.group(2).strip('/')
108 repo_name = git_match.group(2).strip('/')
109 mode = git_match.group(1)
109 mode = git_match.group(1)
110 return vcs_type, repo_name, mode
110 return vcs_type, repo_name, mode
111
111
112 svn_pattern = r'^svnserve -t'
112 svn_pattern = r'^svnserve -t'
113 svn_match = re.match(svn_pattern, self.command)
113 svn_match = re.match(svn_pattern, self.command)
114
114
115 if svn_match is not None:
115 if svn_match is not None:
116 vcs_type = 'svn'
116 vcs_type = 'svn'
117 # Repo name should be extracted from the input stream
117 # Repo name should be extracted from the input stream
118 return vcs_type, repo_name, mode
118 return vcs_type, repo_name, mode
119
119
120 return vcs_type, repo_name, mode
120 return vcs_type, repo_name, mode
121
121
122 def serve(self, vcs, repo, mode, user, permissions):
122 def serve(self, vcs, repo, mode, user, permissions):
123 store = ScmModel().repos_path
123 store = ScmModel().repos_path
124
124
125 log.debug(
125 log.debug(
126 'VCS detected:`%s` mode: `%s` repo_name: %s', vcs, mode, repo)
126 'VCS detected:`%s` mode: `%s` repo_name: %s', vcs, mode, repo)
127
127
128 if vcs == 'hg':
128 if vcs == 'hg':
129 server = MercurialServer(
129 server = MercurialServer(
130 store=store, ini_path=self.ini_path,
130 store=store, ini_path=self.ini_path,
131 repo_name=repo, user=user,
131 repo_name=repo, user=user,
132 user_permissions=permissions, config=self.config, env=self.env)
132 user_permissions=permissions, config=self.config, env=self.env)
133 self.server_impl = server
133 self.server_impl = server
134 return server.run()
134 return server.run()
135
135
136 elif vcs == 'git':
136 elif vcs == 'git':
137 server = GitServer(
137 server = GitServer(
138 store=store, ini_path=self.ini_path,
138 store=store, ini_path=self.ini_path,
139 repo_name=repo, repo_mode=mode, user=user,
139 repo_name=repo, repo_mode=mode, user=user,
140 user_permissions=permissions, config=self.config, env=self.env)
140 user_permissions=permissions, config=self.config, env=self.env)
141 self.server_impl = server
141 self.server_impl = server
142 return server.run()
142 return server.run()
143
143
144 elif vcs == 'svn':
144 elif vcs == 'svn':
145 server = SubversionServer(
145 server = SubversionServer(
146 store=store, ini_path=self.ini_path,
146 store=store, ini_path=self.ini_path,
147 repo_name=None, user=user,
147 repo_name=None, user=user,
148 user_permissions=permissions, config=self.config, env=self.env)
148 user_permissions=permissions, config=self.config, env=self.env)
149 self.server_impl = server
149 self.server_impl = server
150 return server.run()
150 return server.run()
151
151
152 else:
152 else:
153 raise Exception('Unrecognised VCS: {}'.format(vcs))
153 raise Exception('Unrecognised VCS: {}'.format(vcs))
154
154
155 def wrap(self):
155 def wrap(self):
156 mode = self.mode
156 mode = self.mode
157 user = self.user
157 user = self.user
158 user_id = self.user_id
158 user_id = self.user_id
159 key_id = self.key_id
159 key_id = self.key_id
160 shell = self.shell
160 shell = self.shell
161
161
162 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
162 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
163
163
164 log.debug(
164 log.debug(
165 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
165 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
166 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
166 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
167 mode, user, user_id, shell, self.command,
167 mode, user, user_id, shell, self.command,
168 scm_detected, scm_mode, scm_repo)
168 scm_detected, scm_mode, scm_repo)
169
169
170 # update last access time for this key
170 # update last access time for this key
171 self.update_key_access_time(key_id)
171 self.update_key_access_time(key_id)
172
172
173 log.debug('SSH Connection info %s', self.get_connection_info())
173 log.debug('SSH Connection info %s', self.get_connection_info())
174
174
175 if shell and self.command is None:
175 if shell and self.command is None:
176 log.info(
176 log.info(
177 'Dropping to shell, no command given and shell is allowed')
177 'Dropping to shell, no command given and shell is allowed')
178 os.execl('/bin/bash', '-l')
178 os.execl('/bin/bash', '-l')
179 exit_code = 1
179 exit_code = 1
180
180
181 elif scm_detected:
181 elif scm_detected:
182 user = User.get(user_id)
182 user = User.get(user_id)
183 if not user:
184 log.warning('User with id %s not found', user_id)
185 exit_code = -1
186 return exit_code
187
183 auth_user = user.AuthUser()
188 auth_user = user.AuthUser()
184 permissions = auth_user.permissions['repositories']
189 permissions = auth_user.permissions['repositories']
185
190
186 try:
191 try:
187 exit_code, is_updated = self.serve(
192 exit_code, is_updated = self.serve(
188 scm_detected, scm_repo, scm_mode, user, permissions)
193 scm_detected, scm_repo, scm_mode, user, permissions)
189 except Exception:
194 except Exception:
190 log.exception('Error occurred during execution of SshWrapper')
195 log.exception('Error occurred during execution of SshWrapper')
191 exit_code = -1
196 exit_code = -1
192
197
193 elif self.command is None and shell is False:
198 elif self.command is None and shell is False:
194 log.error('No Command given.')
199 log.error('No Command given.')
195 exit_code = -1
200 exit_code = -1
196
201
197 else:
202 else:
198 log.error(
203 log.error(
199 'Unhandled Command: "%s" Aborting.', self.command)
204 'Unhandled Command: "%s" Aborting.', self.command)
200 exit_code = -1
205 exit_code = -1
201
206
202 return exit_code
207 return exit_code
@@ -1,149 +1,150 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 sys
22 import sys
23 import json
23 import json
24 import logging
24 import logging
25
25
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 from rhodecode.lib import hooks_utils
27 from rhodecode.lib import hooks_utils
28 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class VcsServer(object):
33 class VcsServer(object):
34 _path = None # set executable path for hg/git/svn binary
34 _path = None # set executable path for hg/git/svn binary
35 backend = None # set in child classes
35 backend = None # set in child classes
36 tunnel = None # subprocess handling tunnel
36 tunnel = None # subprocess handling tunnel
37 write_perms = ['repository.admin', 'repository.write']
37 write_perms = ['repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39
39
40 def __init__(self, user, user_permissions, config, env):
40 def __init__(self, user, user_permissions, config, env):
41 self.user = user
41 self.user = user
42 self.user_permissions = user_permissions
42 self.user_permissions = user_permissions
43 self.config = config
43 self.config = config
44 self.env = env
44 self.env = env
45 self.stdin = sys.stdin
45 self.stdin = sys.stdin
46
46
47 self.repo_name = None
47 self.repo_name = None
48 self.repo_mode = None
48 self.repo_mode = None
49 self.store = ''
49 self.store = ''
50 self.ini_path = ''
50 self.ini_path = ''
51
51
52 def _invalidate_cache(self, repo_name):
52 def _invalidate_cache(self, repo_name):
53 """
53 """
54 Set's cache for this repository for invalidation on next access
54 Set's cache for this repository for invalidation on next access
55
55
56 :param repo_name: full repo name, also a cache key
56 :param repo_name: full repo name, also a cache key
57 """
57 """
58 ScmModel().mark_for_invalidation(repo_name)
58 ScmModel().mark_for_invalidation(repo_name)
59
59
60 def has_write_perm(self):
60 def has_write_perm(self):
61 permission = self.user_permissions.get(self.repo_name)
61 permission = self.user_permissions.get(self.repo_name)
62 if permission in ['repository.write', 'repository.admin']:
62 if permission in ['repository.write', 'repository.admin']:
63 return True
63 return True
64
64
65 return False
65 return False
66
66
67 def _check_permissions(self, action):
67 def _check_permissions(self, action):
68 permission = self.user_permissions.get(self.repo_name)
68 permission = self.user_permissions.get(self.repo_name)
69 log.debug(
69 log.debug(
70 'permission for %s on %s are: %s',
70 'permission for %s on %s are: %s',
71 self.user, self.repo_name, permission)
71 self.user, self.repo_name, permission)
72
72
73 if action == 'pull':
73 if action == 'pull':
74 if permission in self.read_perms:
74 if permission in self.read_perms:
75 log.info(
75 log.info(
76 'READ Permissions for User "%s" detected to repo "%s"!',
76 'READ Permissions for User "%s" detected to repo "%s"!',
77 self.user, self.repo_name)
77 self.user, self.repo_name)
78 return 0
78 return 0
79 else:
79 else:
80 if permission in self.write_perms:
80 if permission in self.write_perms:
81 log.info(
81 log.info(
82 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
82 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
83 self.user, self.repo_name)
83 self.user, self.repo_name)
84 return 0
84 return 0
85
85
86 log.error('Cannot properly fetch or allow user permissions. '
86 log.error('Cannot properly fetch or allow user %s permissions. '
87 'Return value is: %s, req action: %s', permission, action)
87 'Return value is: %s, req action: %s',
88 self.user, permission, action)
88 return -2
89 return -2
89
90
90 def update_environment(self, action, extras=None):
91 def update_environment(self, action, extras=None):
91
92
92 scm_data = {
93 scm_data = {
93 'ip': os.environ['SSH_CLIENT'].split()[0],
94 'ip': os.environ['SSH_CLIENT'].split()[0],
94 'username': self.user.username,
95 'username': self.user.username,
95 'action': action,
96 'action': action,
96 'repository': self.repo_name,
97 'repository': self.repo_name,
97 'scm': self.backend,
98 'scm': self.backend,
98 'config': self.ini_path,
99 'config': self.ini_path,
99 'make_lock': None,
100 'make_lock': None,
100 'locked_by': [None, None],
101 'locked_by': [None, None],
101 'server_url': None,
102 'server_url': None,
102 'is_shadow_repo': False,
103 'is_shadow_repo': False,
103 'hooks_module': 'rhodecode.lib.hooks_daemon',
104 'hooks_module': 'rhodecode.lib.hooks_daemon',
104 'hooks': ['push', 'pull'],
105 'hooks': ['push', 'pull'],
105 'SSH': True,
106 'SSH': True,
106 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
107 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
107 }
108 }
108 if extras:
109 if extras:
109 scm_data.update(extras)
110 scm_data.update(extras)
110 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
111 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
111
112
112 def get_root_store(self):
113 def get_root_store(self):
113 root_store = self.store
114 root_store = self.store
114 if not root_store.endswith('/'):
115 if not root_store.endswith('/'):
115 # always append trailing slash
116 # always append trailing slash
116 root_store = root_store + '/'
117 root_store = root_store + '/'
117 return root_store
118 return root_store
118
119
119 def _handle_tunnel(self, extras):
120 def _handle_tunnel(self, extras):
120 # pre-auth
121 # pre-auth
121 action = 'pull'
122 action = 'pull'
122 exit_code = self._check_permissions(action)
123 exit_code = self._check_permissions(action)
123 if exit_code:
124 if exit_code:
124 return exit_code, False
125 return exit_code, False
125
126
126 req = self.env['request']
127 req = self.env['request']
127 server_url = req.host_url + req.script_name
128 server_url = req.host_url + req.script_name
128 extras['server_url'] = server_url
129 extras['server_url'] = server_url
129
130
130 log.debug('Using %s binaries from path %s', self.backend, self._path)
131 log.debug('Using %s binaries from path %s', self.backend, self._path)
131 exit_code = self.tunnel.run(extras)
132 exit_code = self.tunnel.run(extras)
132
133
133 return exit_code, action == "push"
134 return exit_code, action == "push"
134
135
135 def run(self):
136 def run(self):
136 extras = {}
137 extras = {}
137 HOOKS_PROTOCOL = self.config.get('app:main', 'vcs.hooks.protocol')
138 HOOKS_PROTOCOL = self.config.get('app:main', 'vcs.hooks.protocol')
138
139
139 callback_daemon, extras = prepare_callback_daemon(
140 callback_daemon, extras = prepare_callback_daemon(
140 extras, protocol=HOOKS_PROTOCOL,
141 extras, protocol=HOOKS_PROTOCOL,
141 use_direct_calls=False)
142 use_direct_calls=False)
142
143
143 with callback_daemon:
144 with callback_daemon:
144 try:
145 try:
145 return self._handle_tunnel(extras)
146 return self._handle_tunnel(extras)
146 finally:
147 finally:
147 log.debug('Running cleanup with cache invalidation')
148 log.debug('Running cleanup with cache invalidation')
148 if self.repo_name:
149 if self.repo_name:
149 self._invalidate_cache(self.repo_name)
150 self._invalidate_cache(self.repo_name)
General Comments 0
You need to be logged in to leave comments. Login now