##// END OF EJS Templates
Fixed middleware to prevent deactivated users from authenticating
Liad Shani -
r1620:41696fc7 beta
parent child Browse files
Show More
@@ -1,291 +1,291 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from dulwich import server as dulserver
32 32
33 33
34 34 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35 35
36 36 def handle(self):
37 37 write = lambda x: self.proto.write_sideband(1, x)
38 38
39 39 graph_walker = dulserver.ProtocolGraphWalker(self,
40 40 self.repo.object_store,
41 41 self.repo.get_peeled)
42 42 objects_iter = self.repo.fetch_objects(
43 43 graph_walker.determine_wants, graph_walker, self.progress,
44 44 get_tagged=self.get_tagged)
45 45
46 46 # Do they want any objects?
47 47 if objects_iter is None or len(objects_iter) == 0:
48 48 return
49 49
50 50 self.progress("counting objects: %d, done.\n" % len(objects_iter))
51 51 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
52 52 objects_iter, len(objects_iter))
53 53 messages = []
54 54 messages.append('thank you for using rhodecode')
55 55
56 56 for msg in messages:
57 57 self.progress(msg + "\n")
58 58 # we are done
59 59 self.proto.write("0000")
60 60
61 61 dulserver.DEFAULT_HANDLERS = {
62 62 'git-upload-pack': SimpleGitUploadPackHandler,
63 63 'git-receive-pack': dulserver.ReceivePackHandler,
64 64 }
65 65
66 66 from dulwich.repo import Repo
67 67 from dulwich.web import HTTPGitApplication
68 68
69 69 from paste.auth.basic import AuthBasicAuthenticator
70 70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
71 71
72 72 from rhodecode.lib import safe_str
73 73 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, get_container_username
74 74 from rhodecode.lib.utils import invalidate_cache, is_valid_repo
75 75 from rhodecode.model.db import User
76 76
77 77 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
78 78
79 79 log = logging.getLogger(__name__)
80 80
81 81
82 82 def is_git(environ):
83 83 """Returns True if request's target is git server.
84 84 ``HTTP_USER_AGENT`` would then have git client version given.
85 85
86 86 :param environ:
87 87 """
88 88 http_user_agent = environ.get('HTTP_USER_AGENT')
89 89 if http_user_agent and http_user_agent.startswith('git'):
90 90 return True
91 91 return False
92 92
93 93
94 94 class SimpleGit(object):
95 95
96 96 def __init__(self, application, config):
97 97 self.application = application
98 98 self.config = config
99 99 # base path of repo locations
100 100 self.basepath = self.config['base_path']
101 101 #authenticate this mercurial request using authfunc
102 102 self.authenticate = AuthBasicAuthenticator('', authfunc)
103 103
104 104 def __call__(self, environ, start_response):
105 105 if not is_git(environ):
106 106 return self.application(environ, start_response)
107 107
108 108 proxy_key = 'HTTP_X_REAL_IP'
109 109 def_key = 'REMOTE_ADDR'
110 110 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
111 111 username = None
112 112 # skip passing error to error controller
113 113 environ['pylons.status_code_redirect'] = True
114 114
115 115 #======================================================================
116 116 # EXTRACT REPOSITORY NAME FROM ENV
117 117 #======================================================================
118 118 try:
119 119 repo_name = self.__get_repository(environ)
120 120 log.debug('Extracted repo name is %s' % repo_name)
121 121 except:
122 122 return HTTPInternalServerError()(environ, start_response)
123 123
124 124 #======================================================================
125 125 # GET ACTION PULL or PUSH
126 126 #======================================================================
127 127 action = self.__get_action(environ)
128 128
129 129 #======================================================================
130 130 # CHECK ANONYMOUS PERMISSION
131 131 #======================================================================
132 132 if action in ['pull', 'push']:
133 133 anonymous_user = self.__get_user('default')
134 134 username = anonymous_user.username
135 135 anonymous_perm = self.__check_permission(action,
136 136 anonymous_user,
137 137 repo_name)
138 138
139 139 if anonymous_perm is not True or anonymous_user.active is False:
140 140 if anonymous_perm is not True:
141 141 log.debug('Not enough credentials to access this '
142 142 'repository as anonymous user')
143 143 if anonymous_user.active is False:
144 144 log.debug('Anonymous access is disabled, running '
145 145 'authentication')
146 146 #==============================================================
147 147 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
148 148 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
149 149 #==============================================================
150 150
151 151 if not get_container_username(environ, self.config):
152 152 self.authenticate.realm = \
153 153 safe_str(self.config['rhodecode_realm'])
154 154 result = self.authenticate(environ)
155 155 if isinstance(result, str):
156 156 AUTH_TYPE.update(environ, 'basic')
157 157 REMOTE_USER.update(environ, result)
158 158 else:
159 159 return result.wsgi_application(environ, start_response)
160 160
161 161 #==============================================================
162 162 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
163 163 # BASIC AUTH
164 164 #==============================================================
165 165
166 166 if action in ['pull', 'push']:
167 167 username = get_container_username(environ, self.config)
168 168 try:
169 169 user = self.__get_user(username)
170 if user is None:
170 if user is None or not user.active:
171 171 return HTTPForbidden()(environ, start_response)
172 172 username = user.username
173 173 except:
174 174 log.error(traceback.format_exc())
175 175 return HTTPInternalServerError()(environ,
176 176 start_response)
177 177
178 178 #check permissions for this repository
179 179 perm = self.__check_permission(action, user,
180 180 repo_name)
181 181 if perm is not True:
182 182 return HTTPForbidden()(environ, start_response)
183 183
184 184 extras = {'ip': ipaddr,
185 185 'username': username,
186 186 'action': action,
187 187 'repository': repo_name}
188 188
189 189 #===================================================================
190 190 # GIT REQUEST HANDLING
191 191 #===================================================================
192 192
193 193 repo_path = safe_str(os.path.join(self.basepath, repo_name))
194 194 log.debug('Repository path is %s' % repo_path)
195 195
196 196 # quick check if that dir exists...
197 197 if is_valid_repo(repo_name, self.basepath) is False:
198 198 return HTTPNotFound()(environ, start_response)
199 199
200 200 try:
201 201 #invalidate cache on push
202 202 if action == 'push':
203 203 self.__invalidate_cache(repo_name)
204 204
205 205 app = self.__make_app(repo_name, repo_path)
206 206 return app(environ, start_response)
207 207 except Exception:
208 208 log.error(traceback.format_exc())
209 209 return HTTPInternalServerError()(environ, start_response)
210 210
211 211 def __make_app(self, repo_name, repo_path):
212 212 """
213 213 Make an wsgi application using dulserver
214 214
215 215 :param repo_name: name of the repository
216 216 :param repo_path: full path to the repository
217 217 """
218 218
219 219 _d = {'/' + repo_name: Repo(repo_path)}
220 220 backend = dulserver.DictBackend(_d)
221 221 gitserve = HTTPGitApplication(backend)
222 222
223 223 return gitserve
224 224
225 225 def __check_permission(self, action, user, repo_name):
226 226 """
227 227 Checks permissions using action (push/pull) user and repository
228 228 name
229 229
230 230 :param action: push or pull action
231 231 :param user: user instance
232 232 :param repo_name: repository name
233 233 """
234 234 if action == 'push':
235 235 if not HasPermissionAnyMiddleware('repository.write',
236 236 'repository.admin')(user,
237 237 repo_name):
238 238 return False
239 239
240 240 else:
241 241 #any other action need at least read permission
242 242 if not HasPermissionAnyMiddleware('repository.read',
243 243 'repository.write',
244 244 'repository.admin')(user,
245 245 repo_name):
246 246 return False
247 247
248 248 return True
249 249
250 250 def __get_repository(self, environ):
251 251 """
252 252 Get's repository name out of PATH_INFO header
253 253
254 254 :param environ: environ where PATH_INFO is stored
255 255 """
256 256 try:
257 257 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
258 258 if repo_name.endswith('/'):
259 259 repo_name = repo_name.rstrip('/')
260 260 except:
261 261 log.error(traceback.format_exc())
262 262 raise
263 263 repo_name = repo_name.split('/')[0]
264 264 return repo_name
265 265
266 266 def __get_user(self, username):
267 267 return User.get_by_username(username)
268 268
269 269 def __get_action(self, environ):
270 270 """Maps git request commands into a pull or push command.
271 271
272 272 :param environ:
273 273 """
274 274 service = environ['QUERY_STRING'].split('=')
275 275 if len(service) > 1:
276 276 service_cmd = service[1]
277 277 mapping = {'git-receive-pack': 'push',
278 278 'git-upload-pack': 'pull',
279 279 }
280 280
281 281 return mapping.get(service_cmd,
282 282 service_cmd if service_cmd else 'other')
283 283 else:
284 284 return 'other'
285 285
286 286 def __invalidate_cache(self, repo_name):
287 287 """we know that some change was made to repositories and we should
288 288 invalidate the cache to see the changes right away but only for
289 289 push requests"""
290 290 invalidate_cache('get_repo_cached_%s' % repo_name)
291 291
@@ -1,290 +1,290 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from mercurial.error import RepoError
32 32 from mercurial.hgweb import hgweb_mod
33 33
34 34 from paste.auth.basic import AuthBasicAuthenticator
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from rhodecode.lib import safe_str
38 38 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, get_container_username
39 39 from rhodecode.lib.utils import make_ui, invalidate_cache, \
40 40 is_valid_repo, ui_sections
41 41 from rhodecode.model.db import User
42 42
43 43 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 def is_mercurial(environ):
49 49 """Returns True if request's target is mercurial server - header
50 50 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
51 51 """
52 52 http_accept = environ.get('HTTP_ACCEPT')
53 53 if http_accept and http_accept.startswith('application/mercurial'):
54 54 return True
55 55 return False
56 56
57 57
58 58 class SimpleHg(object):
59 59
60 60 def __init__(self, application, config):
61 61 self.application = application
62 62 self.config = config
63 63 # base path of repo locations
64 64 self.basepath = self.config['base_path']
65 65 #authenticate this mercurial request using authfunc
66 66 self.authenticate = AuthBasicAuthenticator('', authfunc)
67 67 self.ipaddr = '0.0.0.0'
68 68
69 69 def __call__(self, environ, start_response):
70 70 if not is_mercurial(environ):
71 71 return self.application(environ, start_response)
72 72
73 73 proxy_key = 'HTTP_X_REAL_IP'
74 74 def_key = 'REMOTE_ADDR'
75 75 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
76 76
77 77 # skip passing error to error controller
78 78 environ['pylons.status_code_redirect'] = True
79 79
80 80 #======================================================================
81 81 # EXTRACT REPOSITORY NAME FROM ENV
82 82 #======================================================================
83 83 try:
84 84 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
85 85 log.debug('Extracted repo name is %s' % repo_name)
86 86 except:
87 87 return HTTPInternalServerError()(environ, start_response)
88 88
89 89 #======================================================================
90 90 # GET ACTION PULL or PUSH
91 91 #======================================================================
92 92 action = self.__get_action(environ)
93 93
94 94 #======================================================================
95 95 # CHECK ANONYMOUS PERMISSION
96 96 #======================================================================
97 97 if action in ['pull', 'push']:
98 98 anonymous_user = self.__get_user('default')
99 99
100 100 username = anonymous_user.username
101 101 anonymous_perm = self.__check_permission(action,
102 102 anonymous_user,
103 103 repo_name)
104 104
105 105 if anonymous_perm is not True or anonymous_user.active is False:
106 106 if anonymous_perm is not True:
107 107 log.debug('Not enough credentials to access this '
108 108 'repository as anonymous user')
109 109 if anonymous_user.active is False:
110 110 log.debug('Anonymous access is disabled, running '
111 111 'authentication')
112 112 #==============================================================
113 113 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
114 114 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
115 115 #==============================================================
116 116
117 117 if not get_container_username(environ, self.config):
118 118 self.authenticate.realm = \
119 119 safe_str(self.config['rhodecode_realm'])
120 120 result = self.authenticate(environ)
121 121 if isinstance(result, str):
122 122 AUTH_TYPE.update(environ, 'basic')
123 123 REMOTE_USER.update(environ, result)
124 124 else:
125 125 return result.wsgi_application(environ, start_response)
126 126
127 127 #==============================================================
128 128 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
129 129 # BASIC AUTH
130 130 #==============================================================
131 131
132 132 if action in ['pull', 'push']:
133 133 username = get_container_username(environ, self.config)
134 134 try:
135 135 user = self.__get_user(username)
136 if user is None:
136 if user is None or not user.active:
137 137 return HTTPForbidden()(environ, start_response)
138 138 username = user.username
139 139 except:
140 140 log.error(traceback.format_exc())
141 141 return HTTPInternalServerError()(environ,
142 142 start_response)
143 143
144 144 #check permissions for this repository
145 145 perm = self.__check_permission(action, user,
146 146 repo_name)
147 147 if perm is not True:
148 148 return HTTPForbidden()(environ, start_response)
149 149
150 150 extras = {'ip': ipaddr,
151 151 'username': username,
152 152 'action': action,
153 153 'repository': repo_name}
154 154
155 155 #======================================================================
156 156 # MERCURIAL REQUEST HANDLING
157 157 #======================================================================
158 158
159 159 repo_path = safe_str(os.path.join(self.basepath, repo_name))
160 160 log.debug('Repository path is %s' % repo_path)
161 161
162 162 baseui = make_ui('db')
163 163 self.__inject_extras(repo_path, baseui, extras)
164 164
165 165
166 166 # quick check if that dir exists...
167 167 if is_valid_repo(repo_name, self.basepath) is False:
168 168 return HTTPNotFound()(environ, start_response)
169 169
170 170 try:
171 171 #invalidate cache on push
172 172 if action == 'push':
173 173 self.__invalidate_cache(repo_name)
174 174
175 175 app = self.__make_app(repo_path, baseui, extras)
176 176 return app(environ, start_response)
177 177 except RepoError, e:
178 178 if str(e).find('not found') != -1:
179 179 return HTTPNotFound()(environ, start_response)
180 180 except Exception:
181 181 log.error(traceback.format_exc())
182 182 return HTTPInternalServerError()(environ, start_response)
183 183
184 184 def __make_app(self, repo_name, baseui, extras):
185 185 """
186 186 Make an wsgi application using hgweb, and inject generated baseui
187 187 instance, additionally inject some extras into ui object
188 188 """
189 189 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
190 190
191 191
192 192 def __check_permission(self, action, user, repo_name):
193 193 """
194 194 Checks permissions using action (push/pull) user and repository
195 195 name
196 196
197 197 :param action: push or pull action
198 198 :param user: user instance
199 199 :param repo_name: repository name
200 200 """
201 201 if action == 'push':
202 202 if not HasPermissionAnyMiddleware('repository.write',
203 203 'repository.admin')(user,
204 204 repo_name):
205 205 return False
206 206
207 207 else:
208 208 #any other action need at least read permission
209 209 if not HasPermissionAnyMiddleware('repository.read',
210 210 'repository.write',
211 211 'repository.admin')(user,
212 212 repo_name):
213 213 return False
214 214
215 215 return True
216 216
217 217 def __get_repository(self, environ):
218 218 """
219 219 Get's repository name out of PATH_INFO header
220 220
221 221 :param environ: environ where PATH_INFO is stored
222 222 """
223 223 try:
224 224 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
225 225 if repo_name.endswith('/'):
226 226 repo_name = repo_name.rstrip('/')
227 227 except:
228 228 log.error(traceback.format_exc())
229 229 raise
230 230
231 231 return repo_name
232 232
233 233 def __get_user(self, username):
234 234 return User.get_by_username(username)
235 235
236 236 def __get_action(self, environ):
237 237 """
238 238 Maps mercurial request commands into a clone,pull or push command.
239 239 This should always return a valid command string
240 240
241 241 :param environ:
242 242 """
243 243 mapping = {'changegroup': 'pull',
244 244 'changegroupsubset': 'pull',
245 245 'stream_out': 'pull',
246 246 'listkeys': 'pull',
247 247 'unbundle': 'push',
248 248 'pushkey': 'push', }
249 249 for qry in environ['QUERY_STRING'].split('&'):
250 250 if qry.startswith('cmd'):
251 251 cmd = qry.split('=')[-1]
252 252 if cmd in mapping:
253 253 return mapping[cmd]
254 254 else:
255 255 return 'pull'
256 256
257 257 def __invalidate_cache(self, repo_name):
258 258 """we know that some change was made to repositories and we should
259 259 invalidate the cache to see the changes right away but only for
260 260 push requests"""
261 261 invalidate_cache('get_repo_cached_%s' % repo_name)
262 262
263 263 def __inject_extras(self, repo_path, baseui, extras={}):
264 264 """
265 265 Injects some extra params into baseui instance
266 266
267 267 also overwrites global settings with those takes from local hgrc file
268 268
269 269 :param baseui: baseui instance
270 270 :param extras: dict with extra params to put into baseui
271 271 """
272 272
273 273 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
274 274
275 275 # make our hgweb quiet so it doesn't print output
276 276 baseui.setconfig('ui', 'quiet', 'true')
277 277
278 278 #inject some additional parameters that will be available in ui
279 279 #for hooks
280 280 for k, v in extras.items():
281 281 baseui.setconfig('rhodecode_extras', k, v)
282 282
283 283 repoui = make_ui('file', hgrc, False)
284 284
285 285 if repoui:
286 286 #overwrite our ui instance with the section from hgrc file
287 287 for section in ui_sections:
288 288 for k, v in repoui.configitems(section):
289 289 baseui.setconfig(section, k, v)
290 290
General Comments 0
You need to be logged in to leave comments. Login now