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