##// END OF EJS Templates
ssh: improve logging, and make the UI show last accessed date for key.
marcink -
r2973:40c25cc7 default
parent child Browse files
Show More
@@ -1,207 +1,208 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 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 from pyramid.compat 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 log.debug('Update key `%s` access time', key_id)
65 log.debug('Update key id:`%s` fingerprint:`%s` access time',
66 key_id, key.ssh_key_fingerprint)
66 67
67 68 def get_connection_info(self):
68 69 """
69 70 connection_info
70 71
71 72 Identifies the client and server ends of the connection.
72 73 The variable contains four space-separated values: client IP address,
73 74 client port number, server IP address, and server port number.
74 75 """
75 76 conn = dict(
76 77 client_ip=None,
77 78 client_port=None,
78 79 server_ip=None,
79 80 server_port=None,
80 81 )
81 82
82 83 info = self.connection_info.split(' ')
83 84 if len(info) == 4:
84 85 conn['client_ip'] = info[0]
85 86 conn['client_port'] = info[1]
86 87 conn['server_ip'] = info[2]
87 88 conn['server_port'] = info[3]
88 89
89 90 return conn
90 91
91 92 def get_repo_details(self, mode):
92 93 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
93 94 mode = mode
94 95 repo_name = None
95 96
96 97 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
97 98 hg_match = re.match(hg_pattern, self.command)
98 99 if hg_match is not None:
99 100 vcs_type = 'hg'
100 101 repo_name = hg_match.group(1).strip('/')
101 102 return vcs_type, repo_name, mode
102 103
103 104 git_pattern = (
104 105 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
105 106 git_match = re.match(git_pattern, self.command)
106 107 if git_match is not None:
107 108 vcs_type = 'git'
108 109 repo_name = git_match.group(2).strip('/')
109 110 mode = git_match.group(1)
110 111 return vcs_type, repo_name, mode
111 112
112 113 svn_pattern = r'^svnserve -t'
113 114 svn_match = re.match(svn_pattern, self.command)
114 115
115 116 if svn_match is not None:
116 117 vcs_type = 'svn'
117 118 # Repo name should be extracted from the input stream
118 119 return vcs_type, repo_name, mode
119 120
120 121 return vcs_type, repo_name, mode
121 122
122 123 def serve(self, vcs, repo, mode, user, permissions):
123 124 store = ScmModel().repos_path
124 125
125 126 log.debug(
126 127 'VCS detected:`%s` mode: `%s` repo_name: %s', vcs, mode, repo)
127 128
128 129 if vcs == 'hg':
129 130 server = MercurialServer(
130 131 store=store, ini_path=self.ini_path,
131 132 repo_name=repo, user=user,
132 133 user_permissions=permissions, config=self.config, env=self.env)
133 134 self.server_impl = server
134 135 return server.run()
135 136
136 137 elif vcs == 'git':
137 138 server = GitServer(
138 139 store=store, ini_path=self.ini_path,
139 140 repo_name=repo, repo_mode=mode, user=user,
140 141 user_permissions=permissions, config=self.config, env=self.env)
141 142 self.server_impl = server
142 143 return server.run()
143 144
144 145 elif vcs == 'svn':
145 146 server = SubversionServer(
146 147 store=store, ini_path=self.ini_path,
147 148 repo_name=None, user=user,
148 149 user_permissions=permissions, config=self.config, env=self.env)
149 150 self.server_impl = server
150 151 return server.run()
151 152
152 153 else:
153 154 raise Exception('Unrecognised VCS: {}'.format(vcs))
154 155
155 156 def wrap(self):
156 157 mode = self.mode
157 158 user = self.user
158 159 user_id = self.user_id
159 160 key_id = self.key_id
160 161 shell = self.shell
161 162
162 163 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
163 164
164 165 log.debug(
165 166 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
166 167 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
167 168 mode, user, user_id, shell, self.command,
168 169 scm_detected, scm_mode, scm_repo)
169 170
170 171 # update last access time for this key
171 172 self.update_key_access_time(key_id)
172 173
173 174 log.debug('SSH Connection info %s', self.get_connection_info())
174 175
175 176 if shell and self.command is None:
176 177 log.info(
177 178 'Dropping to shell, no command given and shell is allowed')
178 179 os.execl('/bin/bash', '-l')
179 180 exit_code = 1
180 181
181 182 elif scm_detected:
182 183 user = User.get(user_id)
183 184 if not user:
184 185 log.warning('User with id %s not found', user_id)
185 186 exit_code = -1
186 187 return exit_code
187 188
188 189 auth_user = user.AuthUser()
189 190 permissions = auth_user.permissions['repositories']
190 191
191 192 try:
192 193 exit_code, is_updated = self.serve(
193 194 scm_detected, scm_repo, scm_mode, user, permissions)
194 195 except Exception:
195 196 log.exception('Error occurred during execution of SshWrapper')
196 197 exit_code = -1
197 198
198 199 elif self.command is None and shell is False:
199 200 log.error('No Command given.')
200 201 exit_code = -1
201 202
202 203 else:
203 204 log.error(
204 205 'Unhandled Command: "%s" Aborting.', self.command)
205 206 exit_code = -1
206 207
207 208 return exit_code
@@ -1,151 +1,156 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 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.vcs.conf import settings as vcs_settings
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 if not permission:
74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 self.user, self.repo_name)
76 return -2
77
73 78 if action == 'pull':
74 79 if permission in self.read_perms:
75 80 log.info(
76 81 'READ Permissions for User "%s" detected to repo "%s"!',
77 82 self.user, self.repo_name)
78 83 return 0
79 84 else:
80 85 if permission in self.write_perms:
81 86 log.info(
82 87 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
83 88 self.user, self.repo_name)
84 89 return 0
85 90
86 log.error('Cannot properly fetch or allow user %s permissions. '
87 'Return value is: %s, req action: %s',
91 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 'Permissions: %s, vcs action: %s',
88 93 self.user, permission, action)
89 94 return -2
90 95
91 96 def update_environment(self, action, extras=None):
92 97
93 98 scm_data = {
94 99 'ip': os.environ['SSH_CLIENT'].split()[0],
95 100 'username': self.user.username,
96 101 'user_id': self.user.user_id,
97 102 'action': action,
98 103 'repository': self.repo_name,
99 104 'scm': self.backend,
100 105 'config': self.ini_path,
101 106 'make_lock': None,
102 107 'locked_by': [None, None],
103 108 'server_url': None,
104 109 'is_shadow_repo': False,
105 110 'hooks_module': 'rhodecode.lib.hooks_daemon',
106 111 'hooks': ['push', 'pull'],
107 112 'SSH': True,
108 113 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
109 114 }
110 115 if extras:
111 116 scm_data.update(extras)
112 117 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
113 118
114 119 def get_root_store(self):
115 120 root_store = self.store
116 121 if not root_store.endswith('/'):
117 122 # always append trailing slash
118 123 root_store = root_store + '/'
119 124 return root_store
120 125
121 126 def _handle_tunnel(self, extras):
122 127 # pre-auth
123 128 action = 'pull'
124 129 exit_code = self._check_permissions(action)
125 130 if exit_code:
126 131 return exit_code, False
127 132
128 133 req = self.env['request']
129 134 server_url = req.host_url + req.script_name
130 135 extras['server_url'] = server_url
131 136
132 137 log.debug('Using %s binaries from path %s', self.backend, self._path)
133 138 exit_code = self.tunnel.run(extras)
134 139
135 140 return exit_code, action == "push"
136 141
137 142 def run(self):
138 143 extras = {}
139 144
140 145 callback_daemon, extras = prepare_callback_daemon(
141 146 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
142 147 host=vcs_settings.HOOKS_HOST,
143 148 use_direct_calls=False)
144 149
145 150 with callback_daemon:
146 151 try:
147 152 return self._handle_tunnel(extras)
148 153 finally:
149 154 log.debug('Running cleanup with cache invalidation')
150 155 if self.repo_name:
151 156 self._invalidate_cache(self.repo_name)
@@ -1,87 +1,89 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('SSH Keys')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="sshkeys_wrap">
7 7 <table class="rctable ssh_keys">
8 8 <tr>
9 9 <th>${_('Fingerprint')}</th>
10 10 <th>${_('Description')}</th>
11 <th>${_('Created')}</th>
11 <th>${_('Created on')}</th>
12 <th>${_('Accessed on')}</th>
12 13 <th>${_('Action')}</th>
13 14 </tr>
14 15 % if not c.ssh_enabled:
15 16 <tr><td colspan="4"><div class="">${_('SSH Keys usage is currently disabled, please ask your administrator to enable them.')}</div></td></tr>
16 17 % else:
17 18 %if c.user_ssh_keys:
18 19 %for ssh_key in c.user_ssh_keys:
19 20 <tr class="">
20 21 <td class="">
21 22 <code>${ssh_key.ssh_key_fingerprint}</code>
22 23 </td>
23 24 <td class="td-wrap">${ssh_key.description}</td>
24 25 <td class="td-tags">${h.format_date(ssh_key.created_on)}</td>
26 <td class="td-tags">${h.format_date(ssh_key.accessed_on)}</td>
25 27
26 28 <td class="td-action">
27 29 ${h.secure_form(h.route_path('my_account_ssh_keys_delete'), request=request)}
28 30 ${h.hidden('del_ssh_key', ssh_key.ssh_key_id)}
29 31 <button class="btn btn-link btn-danger" type="submit"
30 32 onclick="return confirm('${_('Confirm to remove ssh key %s') % ssh_key.ssh_key_fingerprint}');">
31 33 ${_('Delete')}
32 34 </button>
33 35 ${h.end_form()}
34 36 </td>
35 37 </tr>
36 38 %endfor
37 39 %else:
38 40 <tr><td colspan="4"><div class="">${_('No additional ssh keys specified')}</div></td></tr>
39 41 %endif
40 42 % endif
41 43 </table>
42 44 </div>
43 45
44 46 % if c.ssh_enabled:
45 47 <div class="user_ssh_keys">
46 48 ${h.secure_form(h.route_path('my_account_ssh_keys_add'), request=request)}
47 49 <div class="form form-vertical">
48 50 <!-- fields -->
49 51 <div class="fields">
50 52 <div class="field">
51 53 <div class="label">
52 54 <label for="new_email">${_('New ssh key')}:</label>
53 55 </div>
54 56 <div class="input">
55 57 ${h.text('description', class_='medium', placeholder=_('Description'))}
56 58 <a href="${h.route_path('my_account_ssh_keys_generate')}">${_('Generate random RSA key')}</a>
57 59 </div>
58 60 </div>
59 61
60 62 <div class="field">
61 63 <div class="textarea text-area editor">
62 64 ${h.textarea('key_data',c.default_key, size=30, placeholder=_("Public key, begins with 'ssh-rsa', 'ssh-dss', 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', or 'ecdsa-sha2-nistp521'"))}
63 65 </div>
64 66 </div>
65 67
66 68 <div class="buttons">
67 69 ${h.submit('save',_('Add'),class_="btn")}
68 70 ${h.reset('reset',_('Reset'),class_="btn")}
69 71 </div>
70 72 % if c.default_key:
71 73 ${_('Click add to use this generate SSH key')}
72 74 % endif
73 75 </div>
74 76 </div>
75 77 ${h.end_form()}
76 78 </div>
77 79 % endif
78 80 </div>
79 81 </div>
80 82
81 83 <script>
82 84
83 85 $(document).ready(function(){
84 86
85 87
86 88 });
87 89 </script>
@@ -1,81 +1,83 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('SSH Keys')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="sshkeys_wrap">
7 7 <table class="rctable ssh_keys">
8 8 <tr>
9 9 <th>${_('Fingerprint')}</th>
10 10 <th>${_('Description')}</th>
11 <th>${_('Created')}</th>
11 <th>${_('Created on')}</th>
12 <th>${_('Accessed on')}</th>
12 13 <th>${_('Action')}</th>
13 14 </tr>
14 15 %if c.user_ssh_keys:
15 16 %for ssh_key in c.user_ssh_keys:
16 17 <tr class="">
17 18 <td class="">
18 19 <code>${ssh_key.ssh_key_fingerprint}</code>
19 20 </td>
20 21 <td class="td-wrap">${ssh_key.description}</td>
21 22 <td class="td-tags">${h.format_date(ssh_key.created_on)}</td>
23 <td class="td-tags">${h.format_date(ssh_key.accessed_on)}</td>
22 24
23 25 <td class="td-action">
24 26 ${h.secure_form(h.route_path('edit_user_ssh_keys_delete', user_id=c.user.user_id), request=request)}
25 27 ${h.hidden('del_ssh_key', ssh_key.ssh_key_id)}
26 28 <button class="btn btn-link btn-danger" type="submit"
27 29 onclick="return confirm('${_('Confirm to remove ssh key %s') % ssh_key.ssh_key_fingerprint}');">
28 30 ${_('Delete')}
29 31 </button>
30 32 ${h.end_form()}
31 33 </td>
32 34 </tr>
33 35 %endfor
34 36 %else:
35 37 <tr><td><div class="ip">${_('No additional ssh keys specified')}</div></td></tr>
36 38 %endif
37 39 </table>
38 40 </div>
39 41
40 42 <div class="user_ssh_keys">
41 43 ${h.secure_form(h.route_path('edit_user_ssh_keys_add', user_id=c.user.user_id), request=request)}
42 44 <div class="form form-vertical">
43 45 <!-- fields -->
44 46 <div class="fields">
45 47 <div class="field">
46 48 <div class="label">
47 49 <label for="new_email">${_('New ssh key')}:</label>
48 50 </div>
49 51 <div class="input">
50 52 ${h.text('description', class_='medium', placeholder=_('Description'))}
51 53 <a href="${h.route_path('edit_user_ssh_keys_generate_keypair', user_id=c.user.user_id)}">${_('Generate random RSA key')}</a>
52 54 </div>
53 55 </div>
54 56
55 57 <div class="field">
56 58 <div class="textarea text-area editor">
57 59 ${h.textarea('key_data',c.default_key, size=30, placeholder=_("Public key, begins with 'ssh-rsa', 'ssh-dss', 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', or 'ecdsa-sha2-nistp521'"))}
58 60 </div>
59 61 </div>
60 62
61 63 <div class="buttons">
62 64 ${h.submit('save',_('Add'),class_="btn")}
63 65 ${h.reset('reset',_('Reset'),class_="btn")}
64 66 </div>
65 67 % if c.default_key:
66 68 ${_('Click add to use this generate SSH key')}
67 69 % endif
68 70 </div>
69 71 </div>
70 72 ${h.end_form()}
71 73 </div>
72 74 </div>
73 75 </div>
74 76
75 77 <script>
76 78
77 79 $(document).ready(function(){
78 80
79 81
80 82 });
81 83 </script>
General Comments 0
You need to be logged in to leave comments. Login now