##// END OF EJS Templates
security bugfix simplehg wasn't checking for permissions on remote commands different than pull or push.
marcink -
r605:72bed562 default
parent child Browse files
Show More
@@ -1,221 +1,225
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 # middleware to handle mercurial api calls
3 # middleware to handle mercurial api calls
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
8 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
9 # of the License or (at your opinion) any later version of the license.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
19 # MA 02110-1301, USA.
20 """
20 """
21 Created on 2010-04-28
21 Created on 2010-04-28
22
22
23 @author: marcink
23 @author: marcink
24 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
24 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
25 It's implemented with basic auth function
25 It's implemented with basic auth function
26 """
26 """
27 from itertools import chain
27 from itertools import chain
28 from mercurial.error import RepoError
28 from mercurial.error import RepoError
29 from mercurial.hgweb import hgweb
29 from mercurial.hgweb import hgweb
30 from mercurial.hgweb.request import wsgiapplication
30 from mercurial.hgweb.request import wsgiapplication
31 from paste.auth.basic import AuthBasicAuthenticator
31 from paste.auth.basic import AuthBasicAuthenticator
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, \
33 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, \
34 get_user_cached
34 get_user_cached
35 from rhodecode.lib.utils import is_mercurial, make_ui, invalidate_cache, \
35 from rhodecode.lib.utils import is_mercurial, make_ui, invalidate_cache, \
36 check_repo_fast, ui_sections
36 check_repo_fast, ui_sections
37 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
37 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 import logging
39 import logging
40 import os
40 import os
41 import traceback
41 import traceback
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 class SimpleHg(object):
45 class SimpleHg(object):
46
46
47 def __init__(self, application, config):
47 def __init__(self, application, config):
48 self.application = application
48 self.application = application
49 self.config = config
49 self.config = config
50 #authenticate this mercurial request using
50 #authenticate this mercurial request using
51 self.authenticate = AuthBasicAuthenticator('', authfunc)
51 self.authenticate = AuthBasicAuthenticator('', authfunc)
52
52
53 def __call__(self, environ, start_response):
53 def __call__(self, environ, start_response):
54 if not is_mercurial(environ):
54 if not is_mercurial(environ):
55 return self.application(environ, start_response)
55 return self.application(environ, start_response)
56
56
57 #===================================================================
57 #===================================================================
58 # AUTHENTICATE THIS MERCURIAL REQUEST
58 # AUTHENTICATE THIS MERCURIAL REQUEST
59 #===================================================================
59 #===================================================================
60 username = REMOTE_USER(environ)
60 username = REMOTE_USER(environ)
61 if not username:
61 if not username:
62 self.authenticate.realm = self.config['rhodecode_realm']
62 self.authenticate.realm = self.config['rhodecode_realm']
63 result = self.authenticate(environ)
63 result = self.authenticate(environ)
64 if isinstance(result, str):
64 if isinstance(result, str):
65 AUTH_TYPE.update(environ, 'basic')
65 AUTH_TYPE.update(environ, 'basic')
66 REMOTE_USER.update(environ, result)
66 REMOTE_USER.update(environ, result)
67 else:
67 else:
68 return result.wsgi_application(environ, start_response)
68 return result.wsgi_application(environ, start_response)
69
69
70 try:
70 try:
71 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
71 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
72 if repo_name.endswith('/'):
72 if repo_name.endswith('/'):
73 repo_name = repo_name.rstrip('/')
73 repo_name = repo_name.rstrip('/')
74 except:
74 except:
75 log.error(traceback.format_exc())
75 log.error(traceback.format_exc())
76 return HTTPInternalServerError()(environ, start_response)
76 return HTTPInternalServerError()(environ, start_response)
77
77
78 #===================================================================
78 #===================================================================
79 # CHECK PERMISSIONS FOR THIS REQUEST
79 # CHECK PERMISSIONS FOR THIS REQUEST
80 #===================================================================
80 #===================================================================
81 action = self.__get_action(environ)
81 action = self.__get_action(environ)
82 if action:
82 if action:
83 username = self.__get_environ_user(environ)
83 username = self.__get_environ_user(environ)
84 try:
84 try:
85 user = self.__get_user(username)
85 user = self.__get_user(username)
86 except:
86 except:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return HTTPInternalServerError()(environ, start_response)
88 return HTTPInternalServerError()(environ, start_response)
89 #check permissions for this repository
89 #check permissions for this repository
90 if action == 'pull':
90
91 if action == 'push':
92 if not HasPermissionAnyMiddleware('repository.write',
93 'repository.admin')\
94 (user, repo_name):
95 return HTTPForbidden()(environ, start_response)
96
97 else:
91 if not HasPermissionAnyMiddleware('repository.read',
98 if not HasPermissionAnyMiddleware('repository.read',
92 'repository.write',
99 'repository.write',
93 'repository.admin')\
100 'repository.admin')\
94 (user, repo_name):
101 (user, repo_name):
95 return HTTPForbidden()(environ, start_response)
102 return HTTPForbidden()(environ, start_response)
96 if action == 'push':
103
97 if not HasPermissionAnyMiddleware('repository.write',
98 'repository.admin')\
99 (user, repo_name):
100 return HTTPForbidden()(environ, start_response)
101
102 #log action
104 #log action
103 proxy_key = 'HTTP_X_REAL_IP'
105 proxy_key = 'HTTP_X_REAL_IP'
104 def_key = 'REMOTE_ADDR'
106 def_key = 'REMOTE_ADDR'
105 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
107 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
106 self.__log_user_action(user, action, repo_name, ipaddr)
108 self.__log_user_action(user, action, repo_name, ipaddr)
107
109
108 #===================================================================
110 #===================================================================
109 # MERCURIAL REQUEST HANDLING
111 # MERCURIAL REQUEST HANDLING
110 #===================================================================
112 #===================================================================
111 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
113 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
112 self.baseui = make_ui('db')
114 self.baseui = make_ui('db')
113 self.basepath = self.config['base_path']
115 self.basepath = self.config['base_path']
114 self.repo_path = os.path.join(self.basepath, repo_name)
116 self.repo_path = os.path.join(self.basepath, repo_name)
115
117
116 #quick check if that dir exists...
118 #quick check if that dir exists...
117 if check_repo_fast(repo_name, self.basepath):
119 if check_repo_fast(repo_name, self.basepath):
118 return HTTPNotFound()(environ, start_response)
120 return HTTPNotFound()(environ, start_response)
119 try:
121 try:
120 app = wsgiapplication(self.__make_app)
122 app = wsgiapplication(self.__make_app)
121 except RepoError, e:
123 except RepoError, e:
122 if str(e).find('not found') != -1:
124 if str(e).find('not found') != -1:
123 return HTTPNotFound()(environ, start_response)
125 return HTTPNotFound()(environ, start_response)
124 except Exception:
126 except Exception:
125 log.error(traceback.format_exc())
127 log.error(traceback.format_exc())
126 return HTTPInternalServerError()(environ, start_response)
128 return HTTPInternalServerError()(environ, start_response)
127
129
128 #invalidate cache on push
130 #invalidate cache on push
129 if action == 'push':
131 if action == 'push':
130 self.__invalidate_cache(repo_name)
132 self.__invalidate_cache(repo_name)
131 messages = []
133 messages = []
132 messages.append('thank you for using rhodecode')
134 messages.append('thank you for using rhodecode')
133
135
134 return self.msg_wrapper(app, environ, start_response, messages)
136 return self.msg_wrapper(app, environ, start_response, messages)
135 else:
137 else:
136 return app(environ, start_response)
138 return app(environ, start_response)
137
139
138
140
139 def msg_wrapper(self, app, environ, start_response, messages=[]):
141 def msg_wrapper(self, app, environ, start_response, messages=[]):
140 """
142 """
141 Wrapper for custom messages that come out of mercurial respond messages
143 Wrapper for custom messages that come out of mercurial respond messages
142 is a list of messages that the user will see at the end of response
144 is a list of messages that the user will see at the end of response
143 from merurial protocol actions that involves remote answers
145 from merurial protocol actions that involves remote answers
144 @param app:
146 :param app:
145 @param environ:
147 :param environ:
146 @param start_response:
148 :param start_response:
147 """
149 """
148 def custom_messages(msg_list):
150 def custom_messages(msg_list):
149 for msg in msg_list:
151 for msg in msg_list:
150 yield msg + '\n'
152 yield msg + '\n'
151 org_response = app(environ, start_response)
153 org_response = app(environ, start_response)
152 return chain(org_response, custom_messages(messages))
154 return chain(org_response, custom_messages(messages))
153
155
154 def __make_app(self):
156 def __make_app(self):
155 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
157 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
156 return self.__load_web_settings(hgserve)
158 return self.__load_web_settings(hgserve)
157
159
158 def __get_environ_user(self, environ):
160 def __get_environ_user(self, environ):
159 return environ.get('REMOTE_USER')
161 return environ.get('REMOTE_USER')
160
162
161 def __get_user(self, username):
163 def __get_user(self, username):
162 return get_user_cached(username)
164 return get_user_cached(username)
163
165
164 def __get_action(self, environ):
166 def __get_action(self, environ):
165 """
167 """
166 Maps mercurial request commands into a pull or push command.
168 Maps mercurial request commands into a pull or push command.
167 @param environ:
169 This should return generally always something
170 :param environ:
168 """
171 """
169 mapping = {'changegroup': 'pull',
172 mapping = {'changegroup': 'pull',
170 'changegroupsubset': 'pull',
173 'changegroupsubset': 'pull',
171 'stream_out': 'pull',
174 'stream_out': 'pull',
172 'listkeys': 'pull',
175 'listkeys': 'pull',
173 'unbundle': 'push',
176 'unbundle': 'push',
174 'pushkey': 'push', }
177 'pushkey': 'push', }
175
176 for qry in environ['QUERY_STRING'].split('&'):
178 for qry in environ['QUERY_STRING'].split('&'):
177 if qry.startswith('cmd'):
179 if qry.startswith('cmd'):
178 cmd = qry.split('=')[-1]
180 cmd = qry.split('=')[-1]
179 if mapping.has_key(cmd):
181 if mapping.has_key(cmd):
180 return mapping[cmd]
182 return mapping[cmd]
181
183 else:
184 return cmd
185
182 def __log_user_action(self, user, action, repo, ipaddr):
186 def __log_user_action(self, user, action, repo, ipaddr):
183 action_logger(user, action, repo, ipaddr)
187 action_logger(user, action, repo, ipaddr)
184
188
185 def __invalidate_cache(self, repo_name):
189 def __invalidate_cache(self, repo_name):
186 """we know that some change was made to repositories and we should
190 """we know that some change was made to repositories and we should
187 invalidate the cache to see the changes right away but only for
191 invalidate the cache to see the changes right away but only for
188 push requests"""
192 push requests"""
189 invalidate_cache('cached_repo_list')
193 invalidate_cache('cached_repo_list')
190 invalidate_cache('full_changelog', repo_name)
194 invalidate_cache('full_changelog', repo_name)
191
195
192
196
193 def __load_web_settings(self, hgserve):
197 def __load_web_settings(self, hgserve):
194 #set the global ui for hgserve
198 #set the global ui for hgserve instance passed
195 hgserve.repo.ui = self.baseui
199 hgserve.repo.ui = self.baseui
196
200
197 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
201 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
198 repoui = make_ui('file', hgrc, False)
202 repoui = make_ui('file', hgrc, False)
199
203
200
204
201 if repoui:
205 if repoui:
202 #overwrite our ui instance with the section from hgrc file
206 #overwrite our ui instance with the section from hgrc file
203 for section in ui_sections:
207 for section in ui_sections:
204 for k, v in repoui.configitems(section):
208 for k, v in repoui.configitems(section):
205 hgserve.repo.ui.setconfig(section, k, v)
209 hgserve.repo.ui.setconfig(section, k, v)
206
210
207 return hgserve
211 return hgserve
208
212
209
213
210
214
211
215
212
216
213
217
214
218
215
219
216
220
217
221
218
222
219
223
220
224
221
225
General Comments 0
You need to be logged in to leave comments. Login now