##// END OF EJS Templates
security: update lastactivity when on audit logs....
marcink -
r2930:a5198975 default
parent child Browse files
Show More
@@ -1,542 +1,543 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 inspect
21 import inspect
22 import itertools
22 import itertools
23 import logging
23 import logging
24 import types
24 import types
25 import fnmatch
25 import fnmatch
26
26
27 import decorator
27 import decorator
28 import venusian
28 import venusian
29 from collections import OrderedDict
29 from collections import OrderedDict
30
30
31 from pyramid.exceptions import ConfigurationError
31 from pyramid.exceptions import ConfigurationError
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34 from pyramid.httpexceptions import HTTPNotFound
34 from pyramid.httpexceptions import HTTPNotFound
35
35
36 from rhodecode.api.exc import (
36 from rhodecode.api.exc import (
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 from rhodecode.apps._base import TemplateArgs
38 from rhodecode.apps._base import TemplateArgs
39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.plugins.utils import get_plugin_settings
43 from rhodecode.lib.plugins.utils import get_plugin_settings
44 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.db import User, UserApiKeys
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 DEFAULT_URL = '/_admin/apiv2'
49 DEFAULT_URL = '/_admin/apiv2'
50
50
51
51
52 def find_methods(jsonrpc_methods, pattern):
52 def find_methods(jsonrpc_methods, pattern):
53 matches = OrderedDict()
53 matches = OrderedDict()
54 if not isinstance(pattern, (list, tuple)):
54 if not isinstance(pattern, (list, tuple)):
55 pattern = [pattern]
55 pattern = [pattern]
56
56
57 for single_pattern in pattern:
57 for single_pattern in pattern:
58 for method_name, method in jsonrpc_methods.items():
58 for method_name, method in jsonrpc_methods.items():
59 if fnmatch.fnmatch(method_name, single_pattern):
59 if fnmatch.fnmatch(method_name, single_pattern):
60 matches[method_name] = method
60 matches[method_name] = method
61 return matches
61 return matches
62
62
63
63
64 class ExtJsonRenderer(object):
64 class ExtJsonRenderer(object):
65 """
65 """
66 Custom renderer that mkaes use of our ext_json lib
66 Custom renderer that mkaes use of our ext_json lib
67
67
68 """
68 """
69
69
70 def __init__(self, serializer=json.dumps, **kw):
70 def __init__(self, serializer=json.dumps, **kw):
71 """ Any keyword arguments will be passed to the ``serializer``
71 """ Any keyword arguments will be passed to the ``serializer``
72 function."""
72 function."""
73 self.serializer = serializer
73 self.serializer = serializer
74 self.kw = kw
74 self.kw = kw
75
75
76 def __call__(self, info):
76 def __call__(self, info):
77 """ Returns a plain JSON-encoded string with content-type
77 """ Returns a plain JSON-encoded string with content-type
78 ``application/json``. The content-type may be overridden by
78 ``application/json``. The content-type may be overridden by
79 setting ``request.response.content_type``."""
79 setting ``request.response.content_type``."""
80
80
81 def _render(value, system):
81 def _render(value, system):
82 request = system.get('request')
82 request = system.get('request')
83 if request is not None:
83 if request is not None:
84 response = request.response
84 response = request.response
85 ct = response.content_type
85 ct = response.content_type
86 if ct == response.default_content_type:
86 if ct == response.default_content_type:
87 response.content_type = 'application/json'
87 response.content_type = 'application/json'
88
88
89 return self.serializer(value, **self.kw)
89 return self.serializer(value, **self.kw)
90
90
91 return _render
91 return _render
92
92
93
93
94 def jsonrpc_response(request, result):
94 def jsonrpc_response(request, result):
95 rpc_id = getattr(request, 'rpc_id', None)
95 rpc_id = getattr(request, 'rpc_id', None)
96 response = request.response
96 response = request.response
97
97
98 # store content_type before render is called
98 # store content_type before render is called
99 ct = response.content_type
99 ct = response.content_type
100
100
101 ret_value = ''
101 ret_value = ''
102 if rpc_id:
102 if rpc_id:
103 ret_value = {
103 ret_value = {
104 'id': rpc_id,
104 'id': rpc_id,
105 'result': result,
105 'result': result,
106 'error': None,
106 'error': None,
107 }
107 }
108
108
109 # fetch deprecation warnings, and store it inside results
109 # fetch deprecation warnings, and store it inside results
110 deprecation = getattr(request, 'rpc_deprecation', None)
110 deprecation = getattr(request, 'rpc_deprecation', None)
111 if deprecation:
111 if deprecation:
112 ret_value['DEPRECATION_WARNING'] = deprecation
112 ret_value['DEPRECATION_WARNING'] = deprecation
113
113
114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 response.body = safe_str(raw_body, response.charset)
115 response.body = safe_str(raw_body, response.charset)
116
116
117 if ct == response.default_content_type:
117 if ct == response.default_content_type:
118 response.content_type = 'application/json'
118 response.content_type = 'application/json'
119
119
120 return response
120 return response
121
121
122
122
123 def jsonrpc_error(request, message, retid=None, code=None):
123 def jsonrpc_error(request, message, retid=None, code=None):
124 """
124 """
125 Generate a Response object with a JSON-RPC error body
125 Generate a Response object with a JSON-RPC error body
126
126
127 :param code:
127 :param code:
128 :param retid:
128 :param retid:
129 :param message:
129 :param message:
130 """
130 """
131 err_dict = {'id': retid, 'result': None, 'error': message}
131 err_dict = {'id': retid, 'result': None, 'error': message}
132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 return Response(
133 return Response(
134 body=body,
134 body=body,
135 status=code,
135 status=code,
136 content_type='application/json'
136 content_type='application/json'
137 )
137 )
138
138
139
139
140 def exception_view(exc, request):
140 def exception_view(exc, request):
141 rpc_id = getattr(request, 'rpc_id', None)
141 rpc_id = getattr(request, 'rpc_id', None)
142
142
143 fault_message = 'undefined error'
143 fault_message = 'undefined error'
144 if isinstance(exc, JSONRPCError):
144 if isinstance(exc, JSONRPCError):
145 fault_message = exc.message
145 fault_message = exc.message
146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
147 elif isinstance(exc, JSONRPCValidationError):
147 elif isinstance(exc, JSONRPCValidationError):
148 colander_exc = exc.colander_exception
148 colander_exc = exc.colander_exception
149 # TODO(marcink): think maybe of nicer way to serialize errors ?
149 # TODO(marcink): think maybe of nicer way to serialize errors ?
150 fault_message = colander_exc.asdict()
150 fault_message = colander_exc.asdict()
151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
152 elif isinstance(exc, JSONRPCForbidden):
152 elif isinstance(exc, JSONRPCForbidden):
153 fault_message = 'Access was denied to this resource.'
153 fault_message = 'Access was denied to this resource.'
154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
155 elif isinstance(exc, HTTPNotFound):
155 elif isinstance(exc, HTTPNotFound):
156 method = request.rpc_method
156 method = request.rpc_method
157 log.debug('json-rpc method `%s` not found in list of '
157 log.debug('json-rpc method `%s` not found in list of '
158 'api calls: %s, rpc_id:%s',
158 'api calls: %s, rpc_id:%s',
159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
160
160
161 similar = 'none'
161 similar = 'none'
162 try:
162 try:
163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
164 similar_found = find_methods(
164 similar_found = find_methods(
165 request.registry.jsonrpc_methods, similar_paterns)
165 request.registry.jsonrpc_methods, similar_paterns)
166 similar = ', '.join(similar_found.keys()) or similar
166 similar = ', '.join(similar_found.keys()) or similar
167 except Exception:
167 except Exception:
168 # make the whole above block safe
168 # make the whole above block safe
169 pass
169 pass
170
170
171 fault_message = "No such method: {}. Similar methods: {}".format(
171 fault_message = "No such method: {}. Similar methods: {}".format(
172 method, similar)
172 method, similar)
173
173
174 return jsonrpc_error(request, fault_message, rpc_id)
174 return jsonrpc_error(request, fault_message, rpc_id)
175
175
176
176
177 def request_view(request):
177 def request_view(request):
178 """
178 """
179 Main request handling method. It handles all logic to call a specific
179 Main request handling method. It handles all logic to call a specific
180 exposed method
180 exposed method
181 """
181 """
182
182
183 # check if we can find this session using api_key, get_by_auth_token
183 # check if we can find this session using api_key, get_by_auth_token
184 # search not expired tokens only
184 # search not expired tokens only
185
185
186 try:
186 try:
187 api_user = User.get_by_auth_token(request.rpc_api_key)
187 api_user = User.get_by_auth_token(request.rpc_api_key)
188
188
189 if api_user is None:
189 if api_user is None:
190 return jsonrpc_error(
190 return jsonrpc_error(
191 request, retid=request.rpc_id, message='Invalid API KEY')
191 request, retid=request.rpc_id, message='Invalid API KEY')
192
192
193 if not api_user.active:
193 if not api_user.active:
194 return jsonrpc_error(
194 return jsonrpc_error(
195 request, retid=request.rpc_id,
195 request, retid=request.rpc_id,
196 message='Request from this user not allowed')
196 message='Request from this user not allowed')
197
197
198 # check if we are allowed to use this IP
198 # check if we are allowed to use this IP
199 auth_u = AuthUser(
199 auth_u = AuthUser(
200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
201 if not auth_u.ip_allowed:
201 if not auth_u.ip_allowed:
202 return jsonrpc_error(
202 return jsonrpc_error(
203 request, retid=request.rpc_id,
203 request, retid=request.rpc_id,
204 message='Request from IP:%s not allowed' % (
204 message='Request from IP:%s not allowed' % (
205 request.rpc_ip_addr,))
205 request.rpc_ip_addr,))
206 else:
206 else:
207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
208
208
209 # register our auth-user
209 # register our auth-user
210 request.rpc_user = auth_u
210 request.rpc_user = auth_u
211 request.environ['rc_auth_user_id'] = auth_u.user_id
211
212
212 # now check if token is valid for API
213 # now check if token is valid for API
213 auth_token = request.rpc_api_key
214 auth_token = request.rpc_api_key
214 token_match = api_user.authenticate_by_token(
215 token_match = api_user.authenticate_by_token(
215 auth_token, roles=[UserApiKeys.ROLE_API])
216 auth_token, roles=[UserApiKeys.ROLE_API])
216 invalid_token = not token_match
217 invalid_token = not token_match
217
218
218 log.debug('Checking if API KEY is valid with proper role')
219 log.debug('Checking if API KEY is valid with proper role')
219 if invalid_token:
220 if invalid_token:
220 return jsonrpc_error(
221 return jsonrpc_error(
221 request, retid=request.rpc_id,
222 request, retid=request.rpc_id,
222 message='API KEY invalid or, has bad role for an API call')
223 message='API KEY invalid or, has bad role for an API call')
223
224
224 except Exception:
225 except Exception:
225 log.exception('Error on API AUTH')
226 log.exception('Error on API AUTH')
226 return jsonrpc_error(
227 return jsonrpc_error(
227 request, retid=request.rpc_id, message='Invalid API KEY')
228 request, retid=request.rpc_id, message='Invalid API KEY')
228
229
229 method = request.rpc_method
230 method = request.rpc_method
230 func = request.registry.jsonrpc_methods[method]
231 func = request.registry.jsonrpc_methods[method]
231
232
232 # now that we have a method, add request._req_params to
233 # now that we have a method, add request._req_params to
233 # self.kargs and dispatch control to WGIController
234 # self.kargs and dispatch control to WGIController
234 argspec = inspect.getargspec(func)
235 argspec = inspect.getargspec(func)
235 arglist = argspec[0]
236 arglist = argspec[0]
236 defaults = map(type, argspec[3] or [])
237 defaults = map(type, argspec[3] or [])
237 default_empty = types.NotImplementedType
238 default_empty = types.NotImplementedType
238
239
239 # kw arguments required by this method
240 # kw arguments required by this method
240 func_kwargs = dict(itertools.izip_longest(
241 func_kwargs = dict(itertools.izip_longest(
241 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242
243
243 # This attribute will need to be first param of a method that uses
244 # This attribute will need to be first param of a method that uses
244 # api_key, which is translated to instance of user at that name
245 # api_key, which is translated to instance of user at that name
245 user_var = 'apiuser'
246 user_var = 'apiuser'
246 request_var = 'request'
247 request_var = 'request'
247
248
248 for arg in [user_var, request_var]:
249 for arg in [user_var, request_var]:
249 if arg not in arglist:
250 if arg not in arglist:
250 return jsonrpc_error(
251 return jsonrpc_error(
251 request,
252 request,
252 retid=request.rpc_id,
253 retid=request.rpc_id,
253 message='This method [%s] does not support '
254 message='This method [%s] does not support '
254 'required parameter `%s`' % (func.__name__, arg))
255 'required parameter `%s`' % (func.__name__, arg))
255
256
256 # get our arglist and check if we provided them as args
257 # get our arglist and check if we provided them as args
257 for arg, default in func_kwargs.items():
258 for arg, default in func_kwargs.items():
258 if arg in [user_var, request_var]:
259 if arg in [user_var, request_var]:
259 # user_var and request_var are pre-hardcoded parameters and we
260 # user_var and request_var are pre-hardcoded parameters and we
260 # don't need to do any translation
261 # don't need to do any translation
261 continue
262 continue
262
263
263 # skip the required param check if it's default value is
264 # skip the required param check if it's default value is
264 # NotImplementedType (default_empty)
265 # NotImplementedType (default_empty)
265 if default == default_empty and arg not in request.rpc_params:
266 if default == default_empty and arg not in request.rpc_params:
266 return jsonrpc_error(
267 return jsonrpc_error(
267 request,
268 request,
268 retid=request.rpc_id,
269 retid=request.rpc_id,
269 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 )
271 )
271
272
272 # sanitize extra passed arguments
273 # sanitize extra passed arguments
273 for k in request.rpc_params.keys()[:]:
274 for k in request.rpc_params.keys()[:]:
274 if k not in func_kwargs:
275 if k not in func_kwargs:
275 del request.rpc_params[k]
276 del request.rpc_params[k]
276
277
277 call_params = request.rpc_params
278 call_params = request.rpc_params
278 call_params.update({
279 call_params.update({
279 'request': request,
280 'request': request,
280 'apiuser': auth_u
281 'apiuser': auth_u
281 })
282 })
282
283
283 # register some common functions for usage
284 # register some common functions for usage
284 attach_context_attributes(
285 attach_context_attributes(
285 TemplateArgs(), request, request.rpc_user.user_id)
286 TemplateArgs(), request, request.rpc_user.user_id)
286
287
287 try:
288 try:
288 ret_value = func(**call_params)
289 ret_value = func(**call_params)
289 return jsonrpc_response(request, ret_value)
290 return jsonrpc_response(request, ret_value)
290 except JSONRPCBaseError:
291 except JSONRPCBaseError:
291 raise
292 raise
292 except Exception:
293 except Exception:
293 log.exception('Unhandled exception occurred on api call: %s', func)
294 log.exception('Unhandled exception occurred on api call: %s', func)
294 return jsonrpc_error(request, retid=request.rpc_id,
295 return jsonrpc_error(request, retid=request.rpc_id,
295 message='Internal server error')
296 message='Internal server error')
296
297
297
298
298 def setup_request(request):
299 def setup_request(request):
299 """
300 """
300 Parse a JSON-RPC request body. It's used inside the predicates method
301 Parse a JSON-RPC request body. It's used inside the predicates method
301 to validate and bootstrap requests for usage in rpc calls.
302 to validate and bootstrap requests for usage in rpc calls.
302
303
303 We need to raise JSONRPCError here if we want to return some errors back to
304 We need to raise JSONRPCError here if we want to return some errors back to
304 user.
305 user.
305 """
306 """
306
307
307 log.debug('Executing setup request: %r', request)
308 log.debug('Executing setup request: %r', request)
308 request.rpc_ip_addr = get_ip_addr(request.environ)
309 request.rpc_ip_addr = get_ip_addr(request.environ)
309 # TODO(marcink): deprecate GET at some point
310 # TODO(marcink): deprecate GET at some point
310 if request.method not in ['POST', 'GET']:
311 if request.method not in ['POST', 'GET']:
311 log.debug('unsupported request method "%s"', request.method)
312 log.debug('unsupported request method "%s"', request.method)
312 raise JSONRPCError(
313 raise JSONRPCError(
313 'unsupported request method "%s". Please use POST' % request.method)
314 'unsupported request method "%s". Please use POST' % request.method)
314
315
315 if 'CONTENT_LENGTH' not in request.environ:
316 if 'CONTENT_LENGTH' not in request.environ:
316 log.debug("No Content-Length")
317 log.debug("No Content-Length")
317 raise JSONRPCError("Empty body, No Content-Length in request")
318 raise JSONRPCError("Empty body, No Content-Length in request")
318
319
319 else:
320 else:
320 length = request.environ['CONTENT_LENGTH']
321 length = request.environ['CONTENT_LENGTH']
321 log.debug('Content-Length: %s', length)
322 log.debug('Content-Length: %s', length)
322
323
323 if length == 0:
324 if length == 0:
324 log.debug("Content-Length is 0")
325 log.debug("Content-Length is 0")
325 raise JSONRPCError("Content-Length is 0")
326 raise JSONRPCError("Content-Length is 0")
326
327
327 raw_body = request.body
328 raw_body = request.body
328 try:
329 try:
329 json_body = json.loads(raw_body)
330 json_body = json.loads(raw_body)
330 except ValueError as e:
331 except ValueError as e:
331 # catch JSON errors Here
332 # catch JSON errors Here
332 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333
334
334 request.rpc_id = json_body.get('id')
335 request.rpc_id = json_body.get('id')
335 request.rpc_method = json_body.get('method')
336 request.rpc_method = json_body.get('method')
336
337
337 # check required base parameters
338 # check required base parameters
338 try:
339 try:
339 api_key = json_body.get('api_key')
340 api_key = json_body.get('api_key')
340 if not api_key:
341 if not api_key:
341 api_key = json_body.get('auth_token')
342 api_key = json_body.get('auth_token')
342
343
343 if not api_key:
344 if not api_key:
344 raise KeyError('api_key or auth_token')
345 raise KeyError('api_key or auth_token')
345
346
346 # TODO(marcink): support passing in token in request header
347 # TODO(marcink): support passing in token in request header
347
348
348 request.rpc_api_key = api_key
349 request.rpc_api_key = api_key
349 request.rpc_id = json_body['id']
350 request.rpc_id = json_body['id']
350 request.rpc_method = json_body['method']
351 request.rpc_method = json_body['method']
351 request.rpc_params = json_body['args'] \
352 request.rpc_params = json_body['args'] \
352 if isinstance(json_body['args'], dict) else {}
353 if isinstance(json_body['args'], dict) else {}
353
354
354 log.debug(
355 log.debug(
355 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 except KeyError as e:
357 except KeyError as e:
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358
359
359 log.debug('setup complete, now handling method:%s rpcid:%s',
360 log.debug('setup complete, now handling method:%s rpcid:%s',
360 request.rpc_method, request.rpc_id, )
361 request.rpc_method, request.rpc_id, )
361
362
362
363
363 class RoutePredicate(object):
364 class RoutePredicate(object):
364 def __init__(self, val, config):
365 def __init__(self, val, config):
365 self.val = val
366 self.val = val
366
367
367 def text(self):
368 def text(self):
368 return 'jsonrpc route = %s' % self.val
369 return 'jsonrpc route = %s' % self.val
369
370
370 phash = text
371 phash = text
371
372
372 def __call__(self, info, request):
373 def __call__(self, info, request):
373 if self.val:
374 if self.val:
374 # potentially setup and bootstrap our call
375 # potentially setup and bootstrap our call
375 setup_request(request)
376 setup_request(request)
376
377
377 # Always return True so that even if it isn't a valid RPC it
378 # Always return True so that even if it isn't a valid RPC it
378 # will fall through to the underlaying handlers like notfound_view
379 # will fall through to the underlaying handlers like notfound_view
379 return True
380 return True
380
381
381
382
382 class NotFoundPredicate(object):
383 class NotFoundPredicate(object):
383 def __init__(self, val, config):
384 def __init__(self, val, config):
384 self.val = val
385 self.val = val
385 self.methods = config.registry.jsonrpc_methods
386 self.methods = config.registry.jsonrpc_methods
386
387
387 def text(self):
388 def text(self):
388 return 'jsonrpc method not found = {}.'.format(self.val)
389 return 'jsonrpc method not found = {}.'.format(self.val)
389
390
390 phash = text
391 phash = text
391
392
392 def __call__(self, info, request):
393 def __call__(self, info, request):
393 return hasattr(request, 'rpc_method')
394 return hasattr(request, 'rpc_method')
394
395
395
396
396 class MethodPredicate(object):
397 class MethodPredicate(object):
397 def __init__(self, val, config):
398 def __init__(self, val, config):
398 self.method = val
399 self.method = val
399
400
400 def text(self):
401 def text(self):
401 return 'jsonrpc method = %s' % self.method
402 return 'jsonrpc method = %s' % self.method
402
403
403 phash = text
404 phash = text
404
405
405 def __call__(self, context, request):
406 def __call__(self, context, request):
406 # we need to explicitly return False here, so pyramid doesn't try to
407 # we need to explicitly return False here, so pyramid doesn't try to
407 # execute our view directly. We need our main handler to execute things
408 # execute our view directly. We need our main handler to execute things
408 return getattr(request, 'rpc_method') == self.method
409 return getattr(request, 'rpc_method') == self.method
409
410
410
411
411 def add_jsonrpc_method(config, view, **kwargs):
412 def add_jsonrpc_method(config, view, **kwargs):
412 # pop the method name
413 # pop the method name
413 method = kwargs.pop('method', None)
414 method = kwargs.pop('method', None)
414
415
415 if method is None:
416 if method is None:
416 raise ConfigurationError(
417 raise ConfigurationError(
417 'Cannot register a JSON-RPC method without specifying the '
418 'Cannot register a JSON-RPC method without specifying the '
418 '"method"')
419 '"method"')
419
420
420 # we define custom predicate, to enable to detect conflicting methods,
421 # we define custom predicate, to enable to detect conflicting methods,
421 # those predicates are kind of "translation" from the decorator variables
422 # those predicates are kind of "translation" from the decorator variables
422 # to internal predicates names
423 # to internal predicates names
423
424
424 kwargs['jsonrpc_method'] = method
425 kwargs['jsonrpc_method'] = method
425
426
426 # register our view into global view store for validation
427 # register our view into global view store for validation
427 config.registry.jsonrpc_methods[method] = view
428 config.registry.jsonrpc_methods[method] = view
428
429
429 # we're using our main request_view handler, here, so each method
430 # we're using our main request_view handler, here, so each method
430 # has a unified handler for itself
431 # has a unified handler for itself
431 config.add_view(request_view, route_name='apiv2', **kwargs)
432 config.add_view(request_view, route_name='apiv2', **kwargs)
432
433
433
434
434 class jsonrpc_method(object):
435 class jsonrpc_method(object):
435 """
436 """
436 decorator that works similar to @add_view_config decorator,
437 decorator that works similar to @add_view_config decorator,
437 but tailored for our JSON RPC
438 but tailored for our JSON RPC
438 """
439 """
439
440
440 venusian = venusian # for testing injection
441 venusian = venusian # for testing injection
441
442
442 def __init__(self, method=None, **kwargs):
443 def __init__(self, method=None, **kwargs):
443 self.method = method
444 self.method = method
444 self.kwargs = kwargs
445 self.kwargs = kwargs
445
446
446 def __call__(self, wrapped):
447 def __call__(self, wrapped):
447 kwargs = self.kwargs.copy()
448 kwargs = self.kwargs.copy()
448 kwargs['method'] = self.method or wrapped.__name__
449 kwargs['method'] = self.method or wrapped.__name__
449 depth = kwargs.pop('_depth', 0)
450 depth = kwargs.pop('_depth', 0)
450
451
451 def callback(context, name, ob):
452 def callback(context, name, ob):
452 config = context.config.with_package(info.module)
453 config = context.config.with_package(info.module)
453 config.add_jsonrpc_method(view=ob, **kwargs)
454 config.add_jsonrpc_method(view=ob, **kwargs)
454
455
455 info = venusian.attach(wrapped, callback, category='pyramid',
456 info = venusian.attach(wrapped, callback, category='pyramid',
456 depth=depth + 1)
457 depth=depth + 1)
457 if info.scope == 'class':
458 if info.scope == 'class':
458 # ensure that attr is set if decorating a class method
459 # ensure that attr is set if decorating a class method
459 kwargs.setdefault('attr', wrapped.__name__)
460 kwargs.setdefault('attr', wrapped.__name__)
460
461
461 kwargs['_info'] = info.codeinfo # fbo action_method
462 kwargs['_info'] = info.codeinfo # fbo action_method
462 return wrapped
463 return wrapped
463
464
464
465
465 class jsonrpc_deprecated_method(object):
466 class jsonrpc_deprecated_method(object):
466 """
467 """
467 Marks method as deprecated, adds log.warning, and inject special key to
468 Marks method as deprecated, adds log.warning, and inject special key to
468 the request variable to mark method as deprecated.
469 the request variable to mark method as deprecated.
469 Also injects special docstring that extract_docs will catch to mark
470 Also injects special docstring that extract_docs will catch to mark
470 method as deprecated.
471 method as deprecated.
471
472
472 :param use_method: specify which method should be used instead of
473 :param use_method: specify which method should be used instead of
473 the decorated one
474 the decorated one
474
475
475 Use like::
476 Use like::
476
477
477 @jsonrpc_method()
478 @jsonrpc_method()
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 def old_func(request, apiuser, arg1, arg2):
480 def old_func(request, apiuser, arg1, arg2):
480 ...
481 ...
481 """
482 """
482
483
483 def __init__(self, use_method, deprecated_at_version):
484 def __init__(self, use_method, deprecated_at_version):
484 self.use_method = use_method
485 self.use_method = use_method
485 self.deprecated_at_version = deprecated_at_version
486 self.deprecated_at_version = deprecated_at_version
486 self.deprecated_msg = ''
487 self.deprecated_msg = ''
487
488
488 def __call__(self, func):
489 def __call__(self, func):
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 method=self.use_method)
491 method=self.use_method)
491
492
492 docstring = """\n
493 docstring = """\n
493 .. deprecated:: {version}
494 .. deprecated:: {version}
494
495
495 {deprecation_message}
496 {deprecation_message}
496
497
497 {original_docstring}
498 {original_docstring}
498 """
499 """
499 func.__doc__ = docstring.format(
500 func.__doc__ = docstring.format(
500 version=self.deprecated_at_version,
501 version=self.deprecated_at_version,
501 deprecation_message=self.deprecated_msg,
502 deprecation_message=self.deprecated_msg,
502 original_docstring=func.__doc__)
503 original_docstring=func.__doc__)
503 return decorator.decorator(self.__wrapper, func)
504 return decorator.decorator(self.__wrapper, func)
504
505
505 def __wrapper(self, func, *fargs, **fkwargs):
506 def __wrapper(self, func, *fargs, **fkwargs):
506 log.warning('DEPRECATED API CALL on function %s, please '
507 log.warning('DEPRECATED API CALL on function %s, please '
507 'use `%s` instead', func, self.use_method)
508 'use `%s` instead', func, self.use_method)
508 # alter function docstring to mark as deprecated, this is picked up
509 # alter function docstring to mark as deprecated, this is picked up
509 # via fabric file that generates API DOC.
510 # via fabric file that generates API DOC.
510 result = func(*fargs, **fkwargs)
511 result = func(*fargs, **fkwargs)
511
512
512 request = fargs[0]
513 request = fargs[0]
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 return result
515 return result
515
516
516
517
517 def includeme(config):
518 def includeme(config):
518 plugin_module = 'rhodecode.api'
519 plugin_module = 'rhodecode.api'
519 plugin_settings = get_plugin_settings(
520 plugin_settings = get_plugin_settings(
520 plugin_module, config.registry.settings)
521 plugin_module, config.registry.settings)
521
522
522 if not hasattr(config.registry, 'jsonrpc_methods'):
523 if not hasattr(config.registry, 'jsonrpc_methods'):
523 config.registry.jsonrpc_methods = OrderedDict()
524 config.registry.jsonrpc_methods = OrderedDict()
524
525
525 # match filter by given method only
526 # match filter by given method only
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527
528
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 serializer=json.dumps, indent=4))
530 serializer=json.dumps, indent=4))
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531
532
532 config.add_route_predicate(
533 config.add_route_predicate(
533 'jsonrpc_call', RoutePredicate)
534 'jsonrpc_call', RoutePredicate)
534
535
535 config.add_route(
536 config.add_route(
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537
538
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 # register some exception handling view
540 # register some exception handling view
540 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
543 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,116 +1,120 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.user import UserModel
25 from rhodecode.model.user import UserModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error, crash, jsonify)
28 build_data, api_call, assert_ok, assert_error, crash, jsonify)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdateUser(object):
32 class TestUpdateUser(object):
33 @pytest.mark.parametrize("name, expected", [
33 @pytest.mark.parametrize("name, expected", [
34 ('firstname', 'new_username'),
34 ('firstname', 'new_username'),
35 ('lastname', 'new_username'),
35 ('lastname', 'new_username'),
36 ('email', 'new_username'),
36 ('email', 'new_username'),
37 ('admin', True),
37 ('admin', True),
38 ('admin', False),
38 ('admin', False),
39 ('extern_type', 'ldap'),
39 ('extern_type', 'ldap'),
40 ('extern_type', None),
40 ('extern_type', None),
41 ('extern_name', 'test'),
41 ('extern_name', 'test'),
42 ('extern_name', None),
42 ('extern_name', None),
43 ('active', False),
43 ('active', False),
44 ('active', True),
44 ('active', True),
45 ('password', 'newpass')
45 ('password', 'newpass')
46 ])
46 ])
47 def test_api_update_user(self, name, expected, user_util):
47 def test_api_update_user(self, name, expected, user_util):
48 usr = user_util.create_user()
48 usr = user_util.create_user()
49
49
50 kw = {name: expected, 'userid': usr.user_id}
50 kw = {name: expected, 'userid': usr.user_id}
51 id_, params = build_data(self.apikey, 'update_user', **kw)
51 id_, params = build_data(self.apikey, 'update_user', **kw)
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53
53
54 ret = {
54 ret = {
55 'msg': 'updated user ID:%s %s' % (usr.user_id, usr.username),
55 'msg': 'updated user ID:%s %s' % (usr.user_id, usr.username),
56 'user': jsonify(
56 'user': jsonify(
57 UserModel()
57 UserModel()
58 .get_by_username(usr.username)
58 .get_by_username(usr.username)
59 .get_api_data(include_secrets=True)
59 .get_api_data(include_secrets=True)
60 )
60 )
61 }
61 }
62
62
63 expected = ret
63 expected = ret
64 assert_ok(id_, expected, given=response.body)
64 assert_ok(id_, expected, given=response.body)
65
65
66 def test_api_update_user_no_changed_params(self):
66 def test_api_update_user_no_changed_params(self):
67 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
67 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
68 ret = jsonify(usr.get_api_data(include_secrets=True))
68 ret = jsonify(usr.get_api_data(include_secrets=True))
69 id_, params = build_data(
69 id_, params = build_data(
70 self.apikey, 'update_user', userid=TEST_USER_ADMIN_LOGIN)
70 self.apikey, 'update_user', userid=TEST_USER_ADMIN_LOGIN)
71
71
72 response = api_call(self.app, params)
72 response = api_call(self.app, params)
73 ret = {
73 ret = {
74 'msg': 'updated user ID:%s %s' % (
74 'msg': 'updated user ID:%s %s' % (
75 usr.user_id, TEST_USER_ADMIN_LOGIN),
75 usr.user_id, TEST_USER_ADMIN_LOGIN),
76 'user': ret
76 'user': ret
77 }
77 }
78 expected = ret
78 expected = ret
79 expected['user']['last_activity'] = response.json['result']['user'][
80 'last_activity']
79 assert_ok(id_, expected, given=response.body)
81 assert_ok(id_, expected, given=response.body)
80
82
81 def test_api_update_user_by_user_id(self):
83 def test_api_update_user_by_user_id(self):
82 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
84 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
83 ret = jsonify(usr.get_api_data(include_secrets=True))
85 ret = jsonify(usr.get_api_data(include_secrets=True))
84 id_, params = build_data(
86 id_, params = build_data(
85 self.apikey, 'update_user', userid=usr.user_id)
87 self.apikey, 'update_user', userid=usr.user_id)
86
88
87 response = api_call(self.app, params)
89 response = api_call(self.app, params)
88 ret = {
90 ret = {
89 'msg': 'updated user ID:%s %s' % (
91 'msg': 'updated user ID:%s %s' % (
90 usr.user_id, TEST_USER_ADMIN_LOGIN),
92 usr.user_id, TEST_USER_ADMIN_LOGIN),
91 'user': ret
93 'user': ret
92 }
94 }
93 expected = ret
95 expected = ret
96 expected['user']['last_activity'] = response.json['result']['user'][
97 'last_activity']
94 assert_ok(id_, expected, given=response.body)
98 assert_ok(id_, expected, given=response.body)
95
99
96 def test_api_update_user_default_user(self):
100 def test_api_update_user_default_user(self):
97 usr = User.get_default_user()
101 usr = User.get_default_user()
98 id_, params = build_data(
102 id_, params = build_data(
99 self.apikey, 'update_user', userid=usr.user_id)
103 self.apikey, 'update_user', userid=usr.user_id)
100
104
101 response = api_call(self.app, params)
105 response = api_call(self.app, params)
102 expected = 'editing default user is forbidden'
106 expected = 'editing default user is forbidden'
103 assert_error(id_, expected, given=response.body)
107 assert_error(id_, expected, given=response.body)
104
108
105 @mock.patch.object(UserModel, 'update_user', crash)
109 @mock.patch.object(UserModel, 'update_user', crash)
106 def test_api_update_user_when_exception_happens(self):
110 def test_api_update_user_when_exception_happens(self):
107 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
111 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
108 ret = jsonify(usr.get_api_data(include_secrets=True))
112 ret = jsonify(usr.get_api_data(include_secrets=True))
109 id_, params = build_data(
113 id_, params = build_data(
110 self.apikey, 'update_user', userid=usr.user_id)
114 self.apikey, 'update_user', userid=usr.user_id)
111
115
112 response = api_call(self.app, params)
116 response = api_call(self.app, params)
113 ret = 'failed to update user `%s`' % (usr.user_id,)
117 ret = 'failed to update user `%s`' % (usr.user_id,)
114
118
115 expected = ret
119 expected = ret
116 assert_error(id_, expected, given=response.body)
120 assert_error(id_, expected, given=response.body)
@@ -1,518 +1,522 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 logging
23 import logging
24 import collections
24 import collections
25 import tempfile
25 import tempfile
26
26
27 from paste.gzipper import make_gzip_middleware
27 from paste.gzipper import make_gzip_middleware
28 import pyramid.events
28 from pyramid.wsgi import wsgiapp
29 from pyramid.wsgi import wsgiapp
29 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.config import Configurator
31 from pyramid.config import Configurator
31 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
32 from pyramid.httpexceptions import (
33 from pyramid.httpexceptions import (
33 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 from pyramid.events import ApplicationCreated
35 from pyramid.renderers import render_to_response
35 from pyramid.renderers import render_to_response
36
36
37 from rhodecode.model import meta
37 from rhodecode.model import meta
38 from rhodecode.config import patches
38 from rhodecode.config import patches
39 from rhodecode.config import utils as config_utils
39 from rhodecode.config import utils as config_utils
40 from rhodecode.config.environment import load_pyramid_environment
40 from rhodecode.config.environment import load_pyramid_environment
41
41
42 import rhodecode.events
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.request import Request
44 from rhodecode.lib.request import Request
44 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.celerylib.loader import configure_celery
49 from rhodecode.lib.celerylib.loader import configure_celery
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 from rhodecode.lib.exc_tracking import store_exception
52 from rhodecode.lib.exc_tracking import store_exception
52 from rhodecode.subscribers import (
53 from rhodecode.subscribers import (
53 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 write_metadata_if_needed, inject_app_settings)
55 write_metadata_if_needed, inject_app_settings)
55
56
56
57
57 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
58
59
59
60
60 def is_http_error(response):
61 def is_http_error(response):
61 # error which should have traceback
62 # error which should have traceback
62 return response.status_code > 499
63 return response.status_code > 499
63
64
64
65
65 def make_pyramid_app(global_config, **settings):
66 def make_pyramid_app(global_config, **settings):
66 """
67 """
67 Constructs the WSGI application based on Pyramid.
68 Constructs the WSGI application based on Pyramid.
68
69
69 Specials:
70 Specials:
70
71
71 * The application can also be integrated like a plugin via the call to
72 * The application can also be integrated like a plugin via the call to
72 `includeme`. This is accompanied with the other utility functions which
73 `includeme`. This is accompanied with the other utility functions which
73 are called. Changing this should be done with great care to not break
74 are called. Changing this should be done with great care to not break
74 cases when these fragments are assembled from another place.
75 cases when these fragments are assembled from another place.
75
76
76 """
77 """
77
78
78 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
79 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
79 # will be replaced by the value of the environment variable "NAME" in this case.
80 # will be replaced by the value of the environment variable "NAME" in this case.
80 environ = {
81 environ = {
81 'ENV_{}'.format(key): value for key, value in os.environ.items()}
82 'ENV_{}'.format(key): value for key, value in os.environ.items()}
82
83
83 global_config = _substitute_values(global_config, environ)
84 global_config = _substitute_values(global_config, environ)
84 settings = _substitute_values(settings, environ)
85 settings = _substitute_values(settings, environ)
85
86
86 sanitize_settings_and_apply_defaults(settings)
87 sanitize_settings_and_apply_defaults(settings)
87
88
88 config = Configurator(settings=settings)
89 config = Configurator(settings=settings)
89
90
90 # Apply compatibility patches
91 # Apply compatibility patches
91 patches.inspect_getargspec()
92 patches.inspect_getargspec()
92
93
93 load_pyramid_environment(global_config, settings)
94 load_pyramid_environment(global_config, settings)
94
95
95 # Static file view comes first
96 # Static file view comes first
96 includeme_first(config)
97 includeme_first(config)
97
98
98 includeme(config)
99 includeme(config)
99
100
100 pyramid_app = config.make_wsgi_app()
101 pyramid_app = config.make_wsgi_app()
101 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
102 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
102 pyramid_app.config = config
103 pyramid_app.config = config
103
104
104 config.configure_celery(global_config['__file__'])
105 config.configure_celery(global_config['__file__'])
105 # creating the app uses a connection - return it after we are done
106 # creating the app uses a connection - return it after we are done
106 meta.Session.remove()
107 meta.Session.remove()
107
108
108 log.info('Pyramid app %s created and configured.', pyramid_app)
109 log.info('Pyramid app %s created and configured.', pyramid_app)
109 return pyramid_app
110 return pyramid_app
110
111
111
112
112 def not_found_view(request):
113 def not_found_view(request):
113 """
114 """
114 This creates the view which should be registered as not-found-view to
115 This creates the view which should be registered as not-found-view to
115 pyramid.
116 pyramid.
116 """
117 """
117
118
118 if not getattr(request, 'vcs_call', None):
119 if not getattr(request, 'vcs_call', None):
119 # handle like regular case with our error_handler
120 # handle like regular case with our error_handler
120 return error_handler(HTTPNotFound(), request)
121 return error_handler(HTTPNotFound(), request)
121
122
122 # handle not found view as a vcs call
123 # handle not found view as a vcs call
123 settings = request.registry.settings
124 settings = request.registry.settings
124 ae_client = getattr(request, 'ae_client', None)
125 ae_client = getattr(request, 'ae_client', None)
125 vcs_app = VCSMiddleware(
126 vcs_app = VCSMiddleware(
126 HTTPNotFound(), request.registry, settings,
127 HTTPNotFound(), request.registry, settings,
127 appenlight_client=ae_client)
128 appenlight_client=ae_client)
128
129
129 return wsgiapp(vcs_app)(None, request)
130 return wsgiapp(vcs_app)(None, request)
130
131
131
132
132 def error_handler(exception, request):
133 def error_handler(exception, request):
133 import rhodecode
134 import rhodecode
134 from rhodecode.lib import helpers
135 from rhodecode.lib import helpers
135
136
136 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
137 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
137
138
138 base_response = HTTPInternalServerError()
139 base_response = HTTPInternalServerError()
139 # prefer original exception for the response since it may have headers set
140 # prefer original exception for the response since it may have headers set
140 if isinstance(exception, HTTPException):
141 if isinstance(exception, HTTPException):
141 base_response = exception
142 base_response = exception
142 elif isinstance(exception, VCSCommunicationError):
143 elif isinstance(exception, VCSCommunicationError):
143 base_response = VCSServerUnavailable()
144 base_response = VCSServerUnavailable()
144
145
145 if is_http_error(base_response):
146 if is_http_error(base_response):
146 log.exception(
147 log.exception(
147 'error occurred handling this request for path: %s', request.path)
148 'error occurred handling this request for path: %s', request.path)
148
149
149 error_explanation = base_response.explanation or str(base_response)
150 error_explanation = base_response.explanation or str(base_response)
150 if base_response.status_code == 404:
151 if base_response.status_code == 404:
151 error_explanation += " Or you don't have permission to access it."
152 error_explanation += " Or you don't have permission to access it."
152 c = AttributeDict()
153 c = AttributeDict()
153 c.error_message = base_response.status
154 c.error_message = base_response.status
154 c.error_explanation = error_explanation
155 c.error_explanation = error_explanation
155 c.visual = AttributeDict()
156 c.visual = AttributeDict()
156
157
157 c.visual.rhodecode_support_url = (
158 c.visual.rhodecode_support_url = (
158 request.registry.settings.get('rhodecode_support_url') or
159 request.registry.settings.get('rhodecode_support_url') or
159 request.route_url('rhodecode_support')
160 request.route_url('rhodecode_support')
160 )
161 )
161 c.redirect_time = 0
162 c.redirect_time = 0
162 c.rhodecode_name = rhodecode_title
163 c.rhodecode_name = rhodecode_title
163 if not c.rhodecode_name:
164 if not c.rhodecode_name:
164 c.rhodecode_name = 'Rhodecode'
165 c.rhodecode_name = 'Rhodecode'
165
166
166 c.causes = []
167 c.causes = []
167 if is_http_error(base_response):
168 if is_http_error(base_response):
168 c.causes.append('Server is overloaded.')
169 c.causes.append('Server is overloaded.')
169 c.causes.append('Server database connection is lost.')
170 c.causes.append('Server database connection is lost.')
170 c.causes.append('Server expected unhandled error.')
171 c.causes.append('Server expected unhandled error.')
171
172
172 if hasattr(base_response, 'causes'):
173 if hasattr(base_response, 'causes'):
173 c.causes = base_response.causes
174 c.causes = base_response.causes
174
175
175 c.messages = helpers.flash.pop_messages(request=request)
176 c.messages = helpers.flash.pop_messages(request=request)
176
177
177 exc_info = sys.exc_info()
178 exc_info = sys.exc_info()
178 c.exception_id = id(exc_info)
179 c.exception_id = id(exc_info)
179 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
180 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
180 or base_response.status_code > 499
181 or base_response.status_code > 499
181 c.exception_id_url = request.route_url(
182 c.exception_id_url = request.route_url(
182 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
183 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
183
184
184 if c.show_exception_id:
185 if c.show_exception_id:
185 store_exception(c.exception_id, exc_info)
186 store_exception(c.exception_id, exc_info)
186
187
187 response = render_to_response(
188 response = render_to_response(
188 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
189 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
189 response=base_response)
190 response=base_response)
190
191
191 return response
192 return response
192
193
193
194
194 def includeme_first(config):
195 def includeme_first(config):
195 # redirect automatic browser favicon.ico requests to correct place
196 # redirect automatic browser favicon.ico requests to correct place
196 def favicon_redirect(context, request):
197 def favicon_redirect(context, request):
197 return HTTPFound(
198 return HTTPFound(
198 request.static_path('rhodecode:public/images/favicon.ico'))
199 request.static_path('rhodecode:public/images/favicon.ico'))
199
200
200 config.add_view(favicon_redirect, route_name='favicon')
201 config.add_view(favicon_redirect, route_name='favicon')
201 config.add_route('favicon', '/favicon.ico')
202 config.add_route('favicon', '/favicon.ico')
202
203
203 def robots_redirect(context, request):
204 def robots_redirect(context, request):
204 return HTTPFound(
205 return HTTPFound(
205 request.static_path('rhodecode:public/robots.txt'))
206 request.static_path('rhodecode:public/robots.txt'))
206
207
207 config.add_view(robots_redirect, route_name='robots')
208 config.add_view(robots_redirect, route_name='robots')
208 config.add_route('robots', '/robots.txt')
209 config.add_route('robots', '/robots.txt')
209
210
210 config.add_static_view(
211 config.add_static_view(
211 '_static/deform', 'deform:static')
212 '_static/deform', 'deform:static')
212 config.add_static_view(
213 config.add_static_view(
213 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
214 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
214
215
215
216
216 def includeme(config):
217 def includeme(config):
217 settings = config.registry.settings
218 settings = config.registry.settings
218 config.set_request_factory(Request)
219 config.set_request_factory(Request)
219
220
220 # plugin information
221 # plugin information
221 config.registry.rhodecode_plugins = collections.OrderedDict()
222 config.registry.rhodecode_plugins = collections.OrderedDict()
222
223
223 config.add_directive(
224 config.add_directive(
224 'register_rhodecode_plugin', register_rhodecode_plugin)
225 'register_rhodecode_plugin', register_rhodecode_plugin)
225
226
226 config.add_directive('configure_celery', configure_celery)
227 config.add_directive('configure_celery', configure_celery)
227
228
228 if asbool(settings.get('appenlight', 'false')):
229 if asbool(settings.get('appenlight', 'false')):
229 config.include('appenlight_client.ext.pyramid_tween')
230 config.include('appenlight_client.ext.pyramid_tween')
230
231
231 # Includes which are required. The application would fail without them.
232 # Includes which are required. The application would fail without them.
232 config.include('pyramid_mako')
233 config.include('pyramid_mako')
233 config.include('pyramid_beaker')
234 config.include('pyramid_beaker')
234 config.include('rhodecode.lib.caches')
235 config.include('rhodecode.lib.caches')
235 config.include('rhodecode.lib.rc_cache')
236 config.include('rhodecode.lib.rc_cache')
236
237
237 config.include('rhodecode.authentication')
238 config.include('rhodecode.authentication')
238 config.include('rhodecode.integrations')
239 config.include('rhodecode.integrations')
239
240
240 # apps
241 # apps
241 config.include('rhodecode.apps._base')
242 config.include('rhodecode.apps._base')
242 config.include('rhodecode.apps.ops')
243 config.include('rhodecode.apps.ops')
243
244
244 config.include('rhodecode.apps.admin')
245 config.include('rhodecode.apps.admin')
245 config.include('rhodecode.apps.channelstream')
246 config.include('rhodecode.apps.channelstream')
246 config.include('rhodecode.apps.login')
247 config.include('rhodecode.apps.login')
247 config.include('rhodecode.apps.home')
248 config.include('rhodecode.apps.home')
248 config.include('rhodecode.apps.journal')
249 config.include('rhodecode.apps.journal')
249 config.include('rhodecode.apps.repository')
250 config.include('rhodecode.apps.repository')
250 config.include('rhodecode.apps.repo_group')
251 config.include('rhodecode.apps.repo_group')
251 config.include('rhodecode.apps.user_group')
252 config.include('rhodecode.apps.user_group')
252 config.include('rhodecode.apps.search')
253 config.include('rhodecode.apps.search')
253 config.include('rhodecode.apps.user_profile')
254 config.include('rhodecode.apps.user_profile')
254 config.include('rhodecode.apps.user_group_profile')
255 config.include('rhodecode.apps.user_group_profile')
255 config.include('rhodecode.apps.my_account')
256 config.include('rhodecode.apps.my_account')
256 config.include('rhodecode.apps.svn_support')
257 config.include('rhodecode.apps.svn_support')
257 config.include('rhodecode.apps.ssh_support')
258 config.include('rhodecode.apps.ssh_support')
258 config.include('rhodecode.apps.gist')
259 config.include('rhodecode.apps.gist')
259
260
260 config.include('rhodecode.apps.debug_style')
261 config.include('rhodecode.apps.debug_style')
261 config.include('rhodecode.tweens')
262 config.include('rhodecode.tweens')
262 config.include('rhodecode.api')
263 config.include('rhodecode.api')
263
264
264 config.add_route(
265 config.add_route(
265 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
266 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
266
267
267 config.add_translation_dirs('rhodecode:i18n/')
268 config.add_translation_dirs('rhodecode:i18n/')
268 settings['default_locale_name'] = settings.get('lang', 'en')
269 settings['default_locale_name'] = settings.get('lang', 'en')
269
270
270 # Add subscribers.
271 # Add subscribers.
271 config.add_subscriber(inject_app_settings, ApplicationCreated)
272 config.add_subscriber(inject_app_settings,
272 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
273 pyramid.events.ApplicationCreated)
273 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
274 config.add_subscriber(scan_repositories_if_enabled,
274 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
275 pyramid.events.ApplicationCreated)
275
276 config.add_subscriber(write_metadata_if_needed,
277 pyramid.events.ApplicationCreated)
278 config.add_subscriber(write_js_routes_if_enabled,
279 pyramid.events.ApplicationCreated)
276
280
277 # request custom methods
281 # request custom methods
278 config.add_request_method(
282 config.add_request_method(
279 'rhodecode.lib.partial_renderer.get_partial_renderer',
283 'rhodecode.lib.partial_renderer.get_partial_renderer',
280 'get_partial_renderer')
284 'get_partial_renderer')
281
285
282 # Set the authorization policy.
286 # Set the authorization policy.
283 authz_policy = ACLAuthorizationPolicy()
287 authz_policy = ACLAuthorizationPolicy()
284 config.set_authorization_policy(authz_policy)
288 config.set_authorization_policy(authz_policy)
285
289
286 # Set the default renderer for HTML templates to mako.
290 # Set the default renderer for HTML templates to mako.
287 config.add_mako_renderer('.html')
291 config.add_mako_renderer('.html')
288
292
289 config.add_renderer(
293 config.add_renderer(
290 name='json_ext',
294 name='json_ext',
291 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
295 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
292
296
293 # include RhodeCode plugins
297 # include RhodeCode plugins
294 includes = aslist(settings.get('rhodecode.includes', []))
298 includes = aslist(settings.get('rhodecode.includes', []))
295 for inc in includes:
299 for inc in includes:
296 config.include(inc)
300 config.include(inc)
297
301
298 # custom not found view, if our pyramid app doesn't know how to handle
302 # custom not found view, if our pyramid app doesn't know how to handle
299 # the request pass it to potential VCS handling ap
303 # the request pass it to potential VCS handling ap
300 config.add_notfound_view(not_found_view)
304 config.add_notfound_view(not_found_view)
301 if not settings.get('debugtoolbar.enabled', False):
305 if not settings.get('debugtoolbar.enabled', False):
302 # disabled debugtoolbar handle all exceptions via the error_handlers
306 # disabled debugtoolbar handle all exceptions via the error_handlers
303 config.add_view(error_handler, context=Exception)
307 config.add_view(error_handler, context=Exception)
304
308
305 # all errors including 403/404/50X
309 # all errors including 403/404/50X
306 config.add_view(error_handler, context=HTTPError)
310 config.add_view(error_handler, context=HTTPError)
307
311
308
312
309 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
313 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
310 """
314 """
311 Apply outer WSGI middlewares around the application.
315 Apply outer WSGI middlewares around the application.
312 """
316 """
313 registry = config.registry
317 registry = config.registry
314 settings = registry.settings
318 settings = registry.settings
315
319
316 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
320 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
317 pyramid_app = HttpsFixup(pyramid_app, settings)
321 pyramid_app = HttpsFixup(pyramid_app, settings)
318
322
319 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
323 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
320 pyramid_app, settings)
324 pyramid_app, settings)
321 registry.ae_client = _ae_client
325 registry.ae_client = _ae_client
322
326
323 if settings['gzip_responses']:
327 if settings['gzip_responses']:
324 pyramid_app = make_gzip_middleware(
328 pyramid_app = make_gzip_middleware(
325 pyramid_app, settings, compress_level=1)
329 pyramid_app, settings, compress_level=1)
326
330
327 # this should be the outer most middleware in the wsgi stack since
331 # this should be the outer most middleware in the wsgi stack since
328 # middleware like Routes make database calls
332 # middleware like Routes make database calls
329 def pyramid_app_with_cleanup(environ, start_response):
333 def pyramid_app_with_cleanup(environ, start_response):
330 try:
334 try:
331 return pyramid_app(environ, start_response)
335 return pyramid_app(environ, start_response)
332 finally:
336 finally:
333 # Dispose current database session and rollback uncommitted
337 # Dispose current database session and rollback uncommitted
334 # transactions.
338 # transactions.
335 meta.Session.remove()
339 meta.Session.remove()
336
340
337 # In a single threaded mode server, on non sqlite db we should have
341 # In a single threaded mode server, on non sqlite db we should have
338 # '0 Current Checked out connections' at the end of a request,
342 # '0 Current Checked out connections' at the end of a request,
339 # if not, then something, somewhere is leaving a connection open
343 # if not, then something, somewhere is leaving a connection open
340 pool = meta.Base.metadata.bind.engine.pool
344 pool = meta.Base.metadata.bind.engine.pool
341 log.debug('sa pool status: %s', pool.status())
345 log.debug('sa pool status: %s', pool.status())
342 log.debug('Request processing finalized')
346 log.debug('Request processing finalized')
343
347
344 return pyramid_app_with_cleanup
348 return pyramid_app_with_cleanup
345
349
346
350
347 def sanitize_settings_and_apply_defaults(settings):
351 def sanitize_settings_and_apply_defaults(settings):
348 """
352 """
349 Applies settings defaults and does all type conversion.
353 Applies settings defaults and does all type conversion.
350
354
351 We would move all settings parsing and preparation into this place, so that
355 We would move all settings parsing and preparation into this place, so that
352 we have only one place left which deals with this part. The remaining parts
356 we have only one place left which deals with this part. The remaining parts
353 of the application would start to rely fully on well prepared settings.
357 of the application would start to rely fully on well prepared settings.
354
358
355 This piece would later be split up per topic to avoid a big fat monster
359 This piece would later be split up per topic to avoid a big fat monster
356 function.
360 function.
357 """
361 """
358
362
359 settings.setdefault('rhodecode.edition', 'Community Edition')
363 settings.setdefault('rhodecode.edition', 'Community Edition')
360
364
361 if 'mako.default_filters' not in settings:
365 if 'mako.default_filters' not in settings:
362 # set custom default filters if we don't have it defined
366 # set custom default filters if we don't have it defined
363 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
367 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
364 settings['mako.default_filters'] = 'h_filter'
368 settings['mako.default_filters'] = 'h_filter'
365
369
366 if 'mako.directories' not in settings:
370 if 'mako.directories' not in settings:
367 mako_directories = settings.setdefault('mako.directories', [
371 mako_directories = settings.setdefault('mako.directories', [
368 # Base templates of the original application
372 # Base templates of the original application
369 'rhodecode:templates',
373 'rhodecode:templates',
370 ])
374 ])
371 log.debug(
375 log.debug(
372 "Using the following Mako template directories: %s",
376 "Using the following Mako template directories: %s",
373 mako_directories)
377 mako_directories)
374
378
375 # Default includes, possible to change as a user
379 # Default includes, possible to change as a user
376 pyramid_includes = settings.setdefault('pyramid.includes', [
380 pyramid_includes = settings.setdefault('pyramid.includes', [
377 'rhodecode.lib.middleware.request_wrapper',
381 'rhodecode.lib.middleware.request_wrapper',
378 ])
382 ])
379 log.debug(
383 log.debug(
380 "Using the following pyramid.includes: %s",
384 "Using the following pyramid.includes: %s",
381 pyramid_includes)
385 pyramid_includes)
382
386
383 # TODO: johbo: Re-think this, usually the call to config.include
387 # TODO: johbo: Re-think this, usually the call to config.include
384 # should allow to pass in a prefix.
388 # should allow to pass in a prefix.
385 settings.setdefault('rhodecode.api.url', '/_admin/api')
389 settings.setdefault('rhodecode.api.url', '/_admin/api')
386
390
387 # Sanitize generic settings.
391 # Sanitize generic settings.
388 _list_setting(settings, 'default_encoding', 'UTF-8')
392 _list_setting(settings, 'default_encoding', 'UTF-8')
389 _bool_setting(settings, 'is_test', 'false')
393 _bool_setting(settings, 'is_test', 'false')
390 _bool_setting(settings, 'gzip_responses', 'false')
394 _bool_setting(settings, 'gzip_responses', 'false')
391
395
392 # Call split out functions that sanitize settings for each topic.
396 # Call split out functions that sanitize settings for each topic.
393 _sanitize_appenlight_settings(settings)
397 _sanitize_appenlight_settings(settings)
394 _sanitize_vcs_settings(settings)
398 _sanitize_vcs_settings(settings)
395 _sanitize_cache_settings(settings)
399 _sanitize_cache_settings(settings)
396
400
397 # configure instance id
401 # configure instance id
398 config_utils.set_instance_id(settings)
402 config_utils.set_instance_id(settings)
399
403
400 return settings
404 return settings
401
405
402
406
403 def _sanitize_appenlight_settings(settings):
407 def _sanitize_appenlight_settings(settings):
404 _bool_setting(settings, 'appenlight', 'false')
408 _bool_setting(settings, 'appenlight', 'false')
405
409
406
410
407 def _sanitize_vcs_settings(settings):
411 def _sanitize_vcs_settings(settings):
408 """
412 """
409 Applies settings defaults and does type conversion for all VCS related
413 Applies settings defaults and does type conversion for all VCS related
410 settings.
414 settings.
411 """
415 """
412 _string_setting(settings, 'vcs.svn.compatible_version', '')
416 _string_setting(settings, 'vcs.svn.compatible_version', '')
413 _string_setting(settings, 'git_rev_filter', '--all')
417 _string_setting(settings, 'git_rev_filter', '--all')
414 _string_setting(settings, 'vcs.hooks.protocol', 'http')
418 _string_setting(settings, 'vcs.hooks.protocol', 'http')
415 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
419 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
416 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
420 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
417 _string_setting(settings, 'vcs.server', '')
421 _string_setting(settings, 'vcs.server', '')
418 _string_setting(settings, 'vcs.server.log_level', 'debug')
422 _string_setting(settings, 'vcs.server.log_level', 'debug')
419 _string_setting(settings, 'vcs.server.protocol', 'http')
423 _string_setting(settings, 'vcs.server.protocol', 'http')
420 _bool_setting(settings, 'startup.import_repos', 'false')
424 _bool_setting(settings, 'startup.import_repos', 'false')
421 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
425 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
422 _bool_setting(settings, 'vcs.server.enable', 'true')
426 _bool_setting(settings, 'vcs.server.enable', 'true')
423 _bool_setting(settings, 'vcs.start_server', 'false')
427 _bool_setting(settings, 'vcs.start_server', 'false')
424 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
428 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
425 _int_setting(settings, 'vcs.connection_timeout', 3600)
429 _int_setting(settings, 'vcs.connection_timeout', 3600)
426
430
427 # Support legacy values of vcs.scm_app_implementation. Legacy
431 # Support legacy values of vcs.scm_app_implementation. Legacy
428 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
432 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
429 # which is now mapped to 'http'.
433 # which is now mapped to 'http'.
430 scm_app_impl = settings['vcs.scm_app_implementation']
434 scm_app_impl = settings['vcs.scm_app_implementation']
431 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
435 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
432 settings['vcs.scm_app_implementation'] = 'http'
436 settings['vcs.scm_app_implementation'] = 'http'
433
437
434
438
435 def _sanitize_cache_settings(settings):
439 def _sanitize_cache_settings(settings):
436 _string_setting(settings, 'cache_dir',
440 _string_setting(settings, 'cache_dir',
437 os.path.join(tempfile.gettempdir(), 'rc_cache'))
441 os.path.join(tempfile.gettempdir(), 'rc_cache'))
438 # cache_perms
442 # cache_perms
439 _string_setting(
443 _string_setting(
440 settings,
444 settings,
441 'rc_cache.cache_perms.backend',
445 'rc_cache.cache_perms.backend',
442 'dogpile.cache.rc.file_namespace')
446 'dogpile.cache.rc.file_namespace')
443 _int_setting(
447 _int_setting(
444 settings,
448 settings,
445 'rc_cache.cache_perms.expiration_time',
449 'rc_cache.cache_perms.expiration_time',
446 60)
450 60)
447 _string_setting(
451 _string_setting(
448 settings,
452 settings,
449 'rc_cache.cache_perms.arguments.filename',
453 'rc_cache.cache_perms.arguments.filename',
450 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
454 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
451
455
452 # cache_repo
456 # cache_repo
453 _string_setting(
457 _string_setting(
454 settings,
458 settings,
455 'rc_cache.cache_repo.backend',
459 'rc_cache.cache_repo.backend',
456 'dogpile.cache.rc.file_namespace')
460 'dogpile.cache.rc.file_namespace')
457 _int_setting(
461 _int_setting(
458 settings,
462 settings,
459 'rc_cache.cache_repo.expiration_time',
463 'rc_cache.cache_repo.expiration_time',
460 60)
464 60)
461 _string_setting(
465 _string_setting(
462 settings,
466 settings,
463 'rc_cache.cache_repo.arguments.filename',
467 'rc_cache.cache_repo.arguments.filename',
464 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
468 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
465
469
466 # sql_cache_short
470 # sql_cache_short
467 _string_setting(
471 _string_setting(
468 settings,
472 settings,
469 'rc_cache.sql_cache_short.backend',
473 'rc_cache.sql_cache_short.backend',
470 'dogpile.cache.rc.memory_lru')
474 'dogpile.cache.rc.memory_lru')
471 _int_setting(
475 _int_setting(
472 settings,
476 settings,
473 'rc_cache.sql_cache_short.expiration_time',
477 'rc_cache.sql_cache_short.expiration_time',
474 30)
478 30)
475 _int_setting(
479 _int_setting(
476 settings,
480 settings,
477 'rc_cache.sql_cache_short.max_size',
481 'rc_cache.sql_cache_short.max_size',
478 10000)
482 10000)
479
483
480
484
481 def _int_setting(settings, name, default):
485 def _int_setting(settings, name, default):
482 settings[name] = int(settings.get(name, default))
486 settings[name] = int(settings.get(name, default))
483
487
484
488
485 def _bool_setting(settings, name, default):
489 def _bool_setting(settings, name, default):
486 input_val = settings.get(name, default)
490 input_val = settings.get(name, default)
487 if isinstance(input_val, unicode):
491 if isinstance(input_val, unicode):
488 input_val = input_val.encode('utf8')
492 input_val = input_val.encode('utf8')
489 settings[name] = asbool(input_val)
493 settings[name] = asbool(input_val)
490
494
491
495
492 def _list_setting(settings, name, default):
496 def _list_setting(settings, name, default):
493 raw_value = settings.get(name, default)
497 raw_value = settings.get(name, default)
494
498
495 old_separator = ','
499 old_separator = ','
496 if old_separator in raw_value:
500 if old_separator in raw_value:
497 # If we get a comma separated list, pass it to our own function.
501 # If we get a comma separated list, pass it to our own function.
498 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
502 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
499 else:
503 else:
500 # Otherwise we assume it uses pyramids space/newline separation.
504 # Otherwise we assume it uses pyramids space/newline separation.
501 settings[name] = aslist(raw_value)
505 settings[name] = aslist(raw_value)
502
506
503
507
504 def _string_setting(settings, name, default, lower=True):
508 def _string_setting(settings, name, default, lower=True):
505 value = settings.get(name, default)
509 value = settings.get(name, default)
506 if lower:
510 if lower:
507 value = value.lower()
511 value = value.lower()
508 settings[name] = value
512 settings[name] = value
509
513
510
514
511 def _substitute_values(mapping, substitutions):
515 def _substitute_values(mapping, substitutions):
512 result = {
516 result = {
513 # Note: Cannot use regular replacements, since they would clash
517 # Note: Cannot use regular replacements, since they would clash
514 # with the implementation of ConfigParser. Using "format" instead.
518 # with the implementation of ConfigParser. Using "format" instead.
515 key: value.format(**substitutions)
519 key: value.format(**substitutions)
516 for key, value in mapping.items()
520 for key, value in mapping.items()
517 }
521 }
518 return result
522 return result
@@ -1,76 +1,78 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 from pyramid.threadlocal import get_current_registry
20 from pyramid.threadlocal import get_current_registry
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
22
22
23
23
24 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
25
25
26
26
27 def trigger(event, registry=None):
27 def trigger(event, registry=None):
28 """
28 """
29 Helper method to send an event. This wraps the pyramid logic to send an
29 Helper method to send an event. This wraps the pyramid logic to send an
30 event.
30 event.
31 """
31 """
32 # For the first step we are using pyramids thread locals here. If the
32 # For the first step we are using pyramids thread locals here. If the
33 # event mechanism works out as a good solution we should think about
33 # event mechanism works out as a good solution we should think about
34 # passing the registry as an argument to get rid of it.
34 # passing the registry as an argument to get rid of it.
35 event_name = event.__class__
36 log.debug('event %s sent for execution', event_name)
35 registry = registry or get_current_registry()
37 registry = registry or get_current_registry()
36 registry.notify(event)
38 registry.notify(event)
37 log.debug('event %s triggered using registry %s', event.__class__, registry)
39 log.debug('event %s triggered using registry %s', event_name, registry)
38
40
39 # Send the events to integrations directly
41 # Send the events to integrations directly
40 from rhodecode.integrations import integrations_event_handler
42 from rhodecode.integrations import integrations_event_handler
41 if isinstance(event, RhodeCodeIntegrationEvent):
43 if isinstance(event, RhodeCodeIntegrationEvent):
42 integrations_event_handler(event)
44 integrations_event_handler(event)
43
45
44
46
45 from rhodecode.events.user import ( # noqa
47 from rhodecode.events.user import ( # noqa
46 UserPreCreate,
48 UserPreCreate,
47 UserPostCreate,
49 UserPostCreate,
48 UserPreUpdate,
50 UserPreUpdate,
49 UserRegistered,
51 UserRegistered,
50 UserPermissionsChange,
52 UserPermissionsChange,
51 )
53 )
52
54
53 from rhodecode.events.repo import ( # noqa
55 from rhodecode.events.repo import ( # noqa
54 RepoEvent,
56 RepoEvent,
55 RepoPreCreateEvent, RepoCreateEvent,
57 RepoPreCreateEvent, RepoCreateEvent,
56 RepoPreDeleteEvent, RepoDeleteEvent,
58 RepoPreDeleteEvent, RepoDeleteEvent,
57 RepoPrePushEvent, RepoPushEvent,
59 RepoPrePushEvent, RepoPushEvent,
58 RepoPrePullEvent, RepoPullEvent,
60 RepoPrePullEvent, RepoPullEvent,
59 )
61 )
60
62
61 from rhodecode.events.repo_group import ( # noqa
63 from rhodecode.events.repo_group import ( # noqa
62 RepoGroupEvent,
64 RepoGroupEvent,
63 RepoGroupCreateEvent,
65 RepoGroupCreateEvent,
64 RepoGroupUpdateEvent,
66 RepoGroupUpdateEvent,
65 RepoGroupDeleteEvent,
67 RepoGroupDeleteEvent,
66 )
68 )
67
69
68 from rhodecode.events.pullrequest import ( # noqa
70 from rhodecode.events.pullrequest import ( # noqa
69 PullRequestEvent,
71 PullRequestEvent,
70 PullRequestCreateEvent,
72 PullRequestCreateEvent,
71 PullRequestUpdateEvent,
73 PullRequestUpdateEvent,
72 PullRequestCommentEvent,
74 PullRequestCommentEvent,
73 PullRequestReviewEvent,
75 PullRequestReviewEvent,
74 PullRequestMergeEvent,
76 PullRequestMergeEvent,
75 PullRequestCloseEvent,
77 PullRequestCloseEvent,
76 )
78 )
@@ -1,264 +1,279 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2018 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 logging
21 import logging
22 import datetime
22 import datetime
23
23
24 from rhodecode.lib.jsonalchemy import JsonRaw
24 from rhodecode.lib.jsonalchemy import JsonRaw
25 from rhodecode.model import meta
25 from rhodecode.model import meta
26 from rhodecode.model.db import User, UserLog, Repository
26 from rhodecode.model.db import User, UserLog, Repository
27
27
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31 # action as key, and expected action_data as value
31 # action as key, and expected action_data as value
32 ACTIONS_V1 = {
32 ACTIONS_V1 = {
33 'user.login.success': {'user_agent': ''},
33 'user.login.success': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
36 'user.register': {},
36 'user.register': {},
37 'user.password.reset_request': {},
37 'user.password.reset_request': {},
38 'user.push': {'user_agent': '', 'commit_ids': []},
38 'user.push': {'user_agent': '', 'commit_ids': []},
39 'user.pull': {'user_agent': ''},
39 'user.pull': {'user_agent': ''},
40
40
41 'user.create': {'data': {}},
41 'user.create': {'data': {}},
42 'user.delete': {'old_data': {}},
42 'user.delete': {'old_data': {}},
43 'user.edit': {'old_data': {}},
43 'user.edit': {'old_data': {}},
44 'user.edit.permissions': {},
44 'user.edit.permissions': {},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 'user.edit.email.add': {'email': ''},
49 'user.edit.email.add': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 'user.edit.password_reset.enabled': {},
53 'user.edit.password_reset.enabled': {},
54 'user.edit.password_reset.disabled': {},
54 'user.edit.password_reset.disabled': {},
55
55
56 'user_group.create': {'data': {}},
56 'user_group.create': {'data': {}},
57 'user_group.delete': {'old_data': {}},
57 'user_group.delete': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
59 'user_group.edit.permissions': {},
59 'user_group.edit.permissions': {},
60 'user_group.edit.member.add': {'user': {}},
60 'user_group.edit.member.add': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
62
62
63 'repo.create': {'data': {}},
63 'repo.create': {'data': {}},
64 'repo.fork': {'data': {}},
64 'repo.fork': {'data': {}},
65 'repo.edit': {'old_data': {}},
65 'repo.edit': {'old_data': {}},
66 'repo.edit.permissions': {},
66 'repo.edit.permissions': {},
67 'repo.delete': {'old_data': {}},
67 'repo.delete': {'old_data': {}},
68 'repo.commit.strip': {'commit_id': ''},
68 'repo.commit.strip': {'commit_id': ''},
69 'repo.archive.download': {'user_agent': '', 'archive_name': '',
69 'repo.archive.download': {'user_agent': '', 'archive_name': '',
70 'archive_spec': '', 'archive_cached': ''},
70 'archive_spec': '', 'archive_cached': ''},
71 'repo.pull_request.create': '',
71 'repo.pull_request.create': '',
72 'repo.pull_request.edit': '',
72 'repo.pull_request.edit': '',
73 'repo.pull_request.delete': '',
73 'repo.pull_request.delete': '',
74 'repo.pull_request.close': '',
74 'repo.pull_request.close': '',
75 'repo.pull_request.merge': '',
75 'repo.pull_request.merge': '',
76 'repo.pull_request.vote': '',
76 'repo.pull_request.vote': '',
77 'repo.pull_request.comment.create': '',
77 'repo.pull_request.comment.create': '',
78 'repo.pull_request.comment.delete': '',
78 'repo.pull_request.comment.delete': '',
79
79
80 'repo.pull_request.reviewer.add': '',
80 'repo.pull_request.reviewer.add': '',
81 'repo.pull_request.reviewer.delete': '',
81 'repo.pull_request.reviewer.delete': '',
82
82
83 'repo.commit.comment.create': {'data': {}},
83 'repo.commit.comment.create': {'data': {}},
84 'repo.commit.comment.delete': {'data': {}},
84 'repo.commit.comment.delete': {'data': {}},
85 'repo.commit.vote': '',
85 'repo.commit.vote': '',
86
86
87 'repo_group.create': {'data': {}},
87 'repo_group.create': {'data': {}},
88 'repo_group.edit': {'old_data': {}},
88 'repo_group.edit': {'old_data': {}},
89 'repo_group.edit.permissions': {},
89 'repo_group.edit.permissions': {},
90 'repo_group.delete': {'old_data': {}},
90 'repo_group.delete': {'old_data': {}},
91 }
91 }
92 ACTIONS = ACTIONS_V1
92 ACTIONS = ACTIONS_V1
93
93
94 SOURCE_WEB = 'source_web'
94 SOURCE_WEB = 'source_web'
95 SOURCE_API = 'source_api'
95 SOURCE_API = 'source_api'
96
96
97
97
98 class UserWrap(object):
98 class UserWrap(object):
99 """
99 """
100 Fake object used to imitate AuthUser
100 Fake object used to imitate AuthUser
101 """
101 """
102
102
103 def __init__(self, user_id=None, username=None, ip_addr=None):
103 def __init__(self, user_id=None, username=None, ip_addr=None):
104 self.user_id = user_id
104 self.user_id = user_id
105 self.username = username
105 self.username = username
106 self.ip_addr = ip_addr
106 self.ip_addr = ip_addr
107
107
108
108
109 class RepoWrap(object):
109 class RepoWrap(object):
110 """
110 """
111 Fake object used to imitate RepoObject that audit logger requires
111 Fake object used to imitate RepoObject that audit logger requires
112 """
112 """
113
113
114 def __init__(self, repo_id=None, repo_name=None):
114 def __init__(self, repo_id=None, repo_name=None):
115 self.repo_id = repo_id
115 self.repo_id = repo_id
116 self.repo_name = repo_name
116 self.repo_name = repo_name
117
117
118
118
119 def _store_log(action_name, action_data, user_id, username, user_data,
119 def _store_log(action_name, action_data, user_id, username, user_data,
120 ip_address, repository_id, repository_name):
120 ip_address, repository_id, repository_name):
121 user_log = UserLog()
121 user_log = UserLog()
122 user_log.version = UserLog.VERSION_2
122 user_log.version = UserLog.VERSION_2
123
123
124 user_log.action = action_name
124 user_log.action = action_name
125 user_log.action_data = action_data or JsonRaw(u'{}')
125 user_log.action_data = action_data or JsonRaw(u'{}')
126
126
127 user_log.user_ip = ip_address
127 user_log.user_ip = ip_address
128
128
129 user_log.user_id = user_id
129 user_log.user_id = user_id
130 user_log.username = username
130 user_log.username = username
131 user_log.user_data = user_data or JsonRaw(u'{}')
131 user_log.user_data = user_data or JsonRaw(u'{}')
132
132
133 user_log.repository_id = repository_id
133 user_log.repository_id = repository_id
134 user_log.repository_name = repository_name
134 user_log.repository_name = repository_name
135
135
136 user_log.action_date = datetime.datetime.now()
136 user_log.action_date = datetime.datetime.now()
137
137
138 return user_log
138 return user_log
139
139
140
140
141 def store_web(*args, **kwargs):
141 def store_web(*args, **kwargs):
142 if 'action_data' not in kwargs:
142 if 'action_data' not in kwargs:
143 kwargs['action_data'] = {}
143 kwargs['action_data'] = {}
144 kwargs['action_data'].update({
144 kwargs['action_data'].update({
145 'source': SOURCE_WEB
145 'source': SOURCE_WEB
146 })
146 })
147 return store(*args, **kwargs)
147 return store(*args, **kwargs)
148
148
149
149
150 def store_api(*args, **kwargs):
150 def store_api(*args, **kwargs):
151 if 'action_data' not in kwargs:
151 if 'action_data' not in kwargs:
152 kwargs['action_data'] = {}
152 kwargs['action_data'] = {}
153 kwargs['action_data'].update({
153 kwargs['action_data'].update({
154 'source': SOURCE_API
154 'source': SOURCE_API
155 })
155 })
156 return store(*args, **kwargs)
156 return store(*args, **kwargs)
157
157
158
158
159 def store(action, user, action_data=None, user_data=None, ip_addr=None,
159 def store(action, user, action_data=None, user_data=None, ip_addr=None,
160 repo=None, sa_session=None, commit=False):
160 repo=None, sa_session=None, commit=False):
161 """
161 """
162 Audit logger for various actions made by users, typically this
162 Audit logger for various actions made by users, typically this
163 results in a call such::
163 results in a call such::
164
164
165 from rhodecode.lib import audit_logger
165 from rhodecode.lib import audit_logger
166
166
167 audit_logger.store(
167 audit_logger.store(
168 'repo.edit', user=self._rhodecode_user)
168 'repo.edit', user=self._rhodecode_user)
169 audit_logger.store(
169 audit_logger.store(
170 'repo.delete', action_data={'data': repo_data},
170 'repo.delete', action_data={'data': repo_data},
171 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
171 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
172
172
173 # repo action
173 # repo action
174 audit_logger.store(
174 audit_logger.store(
175 'repo.delete',
175 'repo.delete',
176 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
176 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
177 repo=audit_logger.RepoWrap(repo_name='some-repo'))
177 repo=audit_logger.RepoWrap(repo_name='some-repo'))
178
178
179 # repo action, when we know and have the repository object already
179 # repo action, when we know and have the repository object already
180 audit_logger.store(
180 audit_logger.store(
181 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
181 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
182 user=self._rhodecode_user,
182 user=self._rhodecode_user,
183 repo=repo_object)
183 repo=repo_object)
184
184
185 # alternative wrapper to the above
185 # alternative wrapper to the above
186 audit_logger.store_web(
186 audit_logger.store_web(
187 'repo.delete', action_data={},
187 'repo.delete', action_data={},
188 user=self._rhodecode_user,
188 user=self._rhodecode_user,
189 repo=repo_object)
189 repo=repo_object)
190
190
191 # without an user ?
191 # without an user ?
192 audit_logger.store(
192 audit_logger.store(
193 'user.login.failure',
193 'user.login.failure',
194 user=audit_logger.UserWrap(
194 user=audit_logger.UserWrap(
195 username=self.request.params.get('username'),
195 username=self.request.params.get('username'),
196 ip_addr=self.request.remote_addr))
196 ip_addr=self.request.remote_addr))
197
197
198 """
198 """
199 from rhodecode.lib.utils2 import safe_unicode
199 from rhodecode.lib.utils2 import safe_unicode
200 from rhodecode.lib.auth import AuthUser
200 from rhodecode.lib.auth import AuthUser
201
201
202 action_spec = ACTIONS.get(action, None)
202 action_spec = ACTIONS.get(action, None)
203 if action_spec is None:
203 if action_spec is None:
204 raise ValueError('Action `{}` is not supported'.format(action))
204 raise ValueError('Action `{}` is not supported'.format(action))
205
205
206 if not sa_session:
206 if not sa_session:
207 sa_session = meta.Session()
207 sa_session = meta.Session()
208
208
209 try:
209 try:
210 username = getattr(user, 'username', None)
210 username = getattr(user, 'username', None)
211 if not username:
211 if not username:
212 pass
212 pass
213
213
214 user_id = getattr(user, 'user_id', None)
214 user_id = getattr(user, 'user_id', None)
215 if not user_id:
215 if not user_id:
216 # maybe we have username ? Try to figure user_id from username
216 # maybe we have username ? Try to figure user_id from username
217 if username:
217 if username:
218 user_id = getattr(
218 user_id = getattr(
219 User.get_by_username(username), 'user_id', None)
219 User.get_by_username(username), 'user_id', None)
220
220
221 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
221 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
222 if not ip_addr:
222 if not ip_addr:
223 pass
223 pass
224
224
225 if not user_data:
225 if not user_data:
226 # try to get this from the auth user
226 # try to get this from the auth user
227 if isinstance(user, AuthUser):
227 if isinstance(user, AuthUser):
228 user_data = {
228 user_data = {
229 'username': user.username,
229 'username': user.username,
230 'email': user.email,
230 'email': user.email,
231 }
231 }
232
232
233 repository_name = getattr(repo, 'repo_name', None)
233 repository_name = getattr(repo, 'repo_name', None)
234 repository_id = getattr(repo, 'repo_id', None)
234 repository_id = getattr(repo, 'repo_id', None)
235 if not repository_id:
235 if not repository_id:
236 # maybe we have repo_name ? Try to figure repo_id from repo_name
236 # maybe we have repo_name ? Try to figure repo_id from repo_name
237 if repository_name:
237 if repository_name:
238 repository_id = getattr(
238 repository_id = getattr(
239 Repository.get_by_repo_name(repository_name), 'repo_id', None)
239 Repository.get_by_repo_name(repository_name), 'repo_id', None)
240
240
241 action_name = safe_unicode(action)
241 action_name = safe_unicode(action)
242 ip_address = safe_unicode(ip_addr)
242 ip_address = safe_unicode(ip_addr)
243
243
244 with sa_session.no_autoflush:
245 update_user_last_activity(sa_session, user_id)
246
244 user_log = _store_log(
247 user_log = _store_log(
245 action_name=action_name,
248 action_name=action_name,
246 action_data=action_data or {},
249 action_data=action_data or {},
247 user_id=user_id,
250 user_id=user_id,
248 username=username,
251 username=username,
249 user_data=user_data or {},
252 user_data=user_data or {},
250 ip_address=ip_address,
253 ip_address=ip_address,
251 repository_id=repository_id,
254 repository_id=repository_id,
252 repository_name=repository_name
255 repository_name=repository_name
253 )
256 )
254
257
255 sa_session.add(user_log)
258 sa_session.add(user_log)
259
256 if commit:
260 if commit:
257 sa_session.commit()
261 sa_session.commit()
258
262
259 entry_id = user_log.entry_id or ''
263 entry_id = user_log.entry_id or ''
260 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
264 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
261 entry_id, action_name, user_id, username, ip_address)
265 entry_id, action_name, user_id, username, ip_address)
262
266
263 except Exception:
267 except Exception:
264 log.exception('AUDIT: failed to store audit log')
268 log.exception('AUDIT: failed to store audit log')
269
270
271 def update_user_last_activity(sa_session, user_id):
272 _last_activity = datetime.datetime.now()
273 try:
274 sa_session.query(User).filter(User.user_id == user_id).update(
275 {"last_activity": _last_activity})
276 log.debug(
277 'updated user `%s` last activity to:%s', user_id, _last_activity)
278 except Exception:
279 log.exception("Failed last activity update")
@@ -1,659 +1,661 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31 from StringIO import StringIO
31 from StringIO import StringIO
32 from lxml import etree
32 from lxml import etree
33
33
34 import time
34 import time
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36
36
37 from pyramid.httpexceptions import (
37 from pyramid.httpexceptions import (
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 from zope.cachedescriptors.property import Lazy as LazyProperty
39 from zope.cachedescriptors.property import Lazy as LazyProperty
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 from rhodecode.lib import caches, rc_cache
43 from rhodecode.lib import caches, rc_cache
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.base import (
45 from rhodecode.lib.base import (
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.backends import base
54 from rhodecode.lib.vcs.backends import base
55
55
56 from rhodecode.model import meta
56 from rhodecode.model import meta
57 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def extract_svn_txn_id(acl_repo_name, data):
65 def extract_svn_txn_id(acl_repo_name, data):
66 """
66 """
67 Helper method for extraction of svn txn_id from submited XML data during
67 Helper method for extraction of svn txn_id from submited XML data during
68 POST operations
68 POST operations
69 """
69 """
70 try:
70 try:
71 root = etree.fromstring(data)
71 root = etree.fromstring(data)
72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 for el in root:
73 for el in root:
74 if el.tag == '{DAV:}source':
74 if el.tag == '{DAV:}source':
75 for sub_el in el:
75 for sub_el in el:
76 if sub_el.tag == '{DAV:}href':
76 if sub_el.tag == '{DAV:}href':
77 match = pat.search(sub_el.text)
77 match = pat.search(sub_el.text)
78 if match:
78 if match:
79 svn_tx_id = match.groupdict()['txn_id']
79 svn_tx_id = match.groupdict()['txn_id']
80 txn_id = caches.compute_key_from_params(
80 txn_id = caches.compute_key_from_params(
81 acl_repo_name, svn_tx_id)
81 acl_repo_name, svn_tx_id)
82 return txn_id
82 return txn_id
83 except Exception:
83 except Exception:
84 log.exception('Failed to extract txn_id')
84 log.exception('Failed to extract txn_id')
85
85
86
86
87 def initialize_generator(factory):
87 def initialize_generator(factory):
88 """
88 """
89 Initializes the returned generator by draining its first element.
89 Initializes the returned generator by draining its first element.
90
90
91 This can be used to give a generator an initializer, which is the code
91 This can be used to give a generator an initializer, which is the code
92 up to the first yield statement. This decorator enforces that the first
92 up to the first yield statement. This decorator enforces that the first
93 produced element has the value ``"__init__"`` to make its special
93 produced element has the value ``"__init__"`` to make its special
94 purpose very explicit in the using code.
94 purpose very explicit in the using code.
95 """
95 """
96
96
97 @wraps(factory)
97 @wraps(factory)
98 def wrapper(*args, **kwargs):
98 def wrapper(*args, **kwargs):
99 gen = factory(*args, **kwargs)
99 gen = factory(*args, **kwargs)
100 try:
100 try:
101 init = gen.next()
101 init = gen.next()
102 except StopIteration:
102 except StopIteration:
103 raise ValueError('Generator must yield at least one element.')
103 raise ValueError('Generator must yield at least one element.')
104 if init != "__init__":
104 if init != "__init__":
105 raise ValueError('First yielded element must be "__init__".')
105 raise ValueError('First yielded element must be "__init__".')
106 return gen
106 return gen
107 return wrapper
107 return wrapper
108
108
109
109
110 class SimpleVCS(object):
110 class SimpleVCS(object):
111 """Common functionality for SCM HTTP handlers."""
111 """Common functionality for SCM HTTP handlers."""
112
112
113 SCM = 'unknown'
113 SCM = 'unknown'
114
114
115 acl_repo_name = None
115 acl_repo_name = None
116 url_repo_name = None
116 url_repo_name = None
117 vcs_repo_name = None
117 vcs_repo_name = None
118 rc_extras = {}
118 rc_extras = {}
119
119
120 # We have to handle requests to shadow repositories different than requests
120 # We have to handle requests to shadow repositories different than requests
121 # to normal repositories. Therefore we have to distinguish them. To do this
121 # to normal repositories. Therefore we have to distinguish them. To do this
122 # we use this regex which will match only on URLs pointing to shadow
122 # we use this regex which will match only on URLs pointing to shadow
123 # repositories.
123 # repositories.
124 shadow_repo_re = re.compile(
124 shadow_repo_re = re.compile(
125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 '(?P<target>{slug_pat})/' # target repo
126 '(?P<target>{slug_pat})/' # target repo
127 'pull-request/(?P<pr_id>\d+)/' # pull request
127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 'repository$' # shadow repo
128 'repository$' # shadow repo
129 .format(slug_pat=SLUG_RE.pattern))
129 .format(slug_pat=SLUG_RE.pattern))
130
130
131 def __init__(self, config, registry):
131 def __init__(self, config, registry):
132 self.registry = registry
132 self.registry = registry
133 self.config = config
133 self.config = config
134 # re-populated by specialized middleware
134 # re-populated by specialized middleware
135 self.repo_vcs_config = base.Config()
135 self.repo_vcs_config = base.Config()
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137
137
138 registry.rhodecode_settings = self.rhodecode_settings
138 registry.rhodecode_settings = self.rhodecode_settings
139 # authenticate this VCS request using authfunc
139 # authenticate this VCS request using authfunc
140 auth_ret_code_detection = \
140 auth_ret_code_detection = \
141 str2bool(self.config.get('auth_ret_code_detection', False))
141 str2bool(self.config.get('auth_ret_code_detection', False))
142 self.authenticate = BasicAuth(
142 self.authenticate = BasicAuth(
143 '', authenticate, registry, config.get('auth_ret_code'),
143 '', authenticate, registry, config.get('auth_ret_code'),
144 auth_ret_code_detection)
144 auth_ret_code_detection)
145 self.ip_addr = '0.0.0.0'
145 self.ip_addr = '0.0.0.0'
146
146
147 @LazyProperty
147 @LazyProperty
148 def global_vcs_config(self):
148 def global_vcs_config(self):
149 try:
149 try:
150 return VcsSettingsModel().get_ui_settings_as_config_obj()
150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 except Exception:
151 except Exception:
152 return base.Config()
152 return base.Config()
153
153
154 @property
154 @property
155 def base_path(self):
155 def base_path(self):
156 settings_path = self.repo_vcs_config.get(
156 settings_path = self.repo_vcs_config.get(
157 *VcsSettingsModel.PATH_SETTING)
157 *VcsSettingsModel.PATH_SETTING)
158
158
159 if not settings_path:
159 if not settings_path:
160 settings_path = self.global_vcs_config.get(
160 settings_path = self.global_vcs_config.get(
161 *VcsSettingsModel.PATH_SETTING)
161 *VcsSettingsModel.PATH_SETTING)
162
162
163 if not settings_path:
163 if not settings_path:
164 # try, maybe we passed in explicitly as config option
164 # try, maybe we passed in explicitly as config option
165 settings_path = self.config.get('base_path')
165 settings_path = self.config.get('base_path')
166
166
167 if not settings_path:
167 if not settings_path:
168 raise ValueError('FATAL: base_path is empty')
168 raise ValueError('FATAL: base_path is empty')
169 return settings_path
169 return settings_path
170
170
171 def set_repo_names(self, environ):
171 def set_repo_names(self, environ):
172 """
172 """
173 This will populate the attributes acl_repo_name, url_repo_name,
173 This will populate the attributes acl_repo_name, url_repo_name,
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 shadow) repositories all names are equal. In case of requests to a
175 shadow) repositories all names are equal. In case of requests to a
176 shadow repository the acl-name points to the target repo of the pull
176 shadow repository the acl-name points to the target repo of the pull
177 request and the vcs-name points to the shadow repo file system path.
177 request and the vcs-name points to the shadow repo file system path.
178 The url-name is always the URL used by the vcs client program.
178 The url-name is always the URL used by the vcs client program.
179
179
180 Example in case of a shadow repo:
180 Example in case of a shadow repo:
181 acl_repo_name = RepoGroup/MyRepo
181 acl_repo_name = RepoGroup/MyRepo
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 """
184 """
185 # First we set the repo name from URL for all attributes. This is the
185 # First we set the repo name from URL for all attributes. This is the
186 # default if handling normal (non shadow) repo requests.
186 # default if handling normal (non shadow) repo requests.
187 self.url_repo_name = self._get_repository_name(environ)
187 self.url_repo_name = self._get_repository_name(environ)
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 self.is_shadow_repo = False
189 self.is_shadow_repo = False
190
190
191 # Check if this is a request to a shadow repository.
191 # Check if this is a request to a shadow repository.
192 match = self.shadow_repo_re.match(self.url_repo_name)
192 match = self.shadow_repo_re.match(self.url_repo_name)
193 if match:
193 if match:
194 match_dict = match.groupdict()
194 match_dict = match.groupdict()
195
195
196 # Build acl repo name from regex match.
196 # Build acl repo name from regex match.
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
198 groups=match_dict['groups'] or '',
198 groups=match_dict['groups'] or '',
199 target=match_dict['target']))
199 target=match_dict['target']))
200
200
201 # Retrieve pull request instance by ID from regex match.
201 # Retrieve pull request instance by ID from regex match.
202 pull_request = PullRequest.get(match_dict['pr_id'])
202 pull_request = PullRequest.get(match_dict['pr_id'])
203
203
204 # Only proceed if we got a pull request and if acl repo name from
204 # Only proceed if we got a pull request and if acl repo name from
205 # URL equals the target repo name of the pull request.
205 # URL equals the target repo name of the pull request.
206 if pull_request and \
206 if pull_request and \
207 (acl_repo_name == pull_request.target_repo.repo_name):
207 (acl_repo_name == pull_request.target_repo.repo_name):
208 repo_id = pull_request.target_repo.repo_id
208 repo_id = pull_request.target_repo.repo_id
209 # Get file system path to shadow repository.
209 # Get file system path to shadow repository.
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
211 target_vcs = pull_request.target_repo.scm_instance()
211 target_vcs = pull_request.target_repo.scm_instance()
212 vcs_repo_name = target_vcs._get_shadow_repository_path(
212 vcs_repo_name = target_vcs._get_shadow_repository_path(
213 repo_id, workspace_id)
213 repo_id, workspace_id)
214
214
215 # Store names for later usage.
215 # Store names for later usage.
216 self.vcs_repo_name = vcs_repo_name
216 self.vcs_repo_name = vcs_repo_name
217 self.acl_repo_name = acl_repo_name
217 self.acl_repo_name = acl_repo_name
218 self.is_shadow_repo = True
218 self.is_shadow_repo = True
219
219
220 log.debug('Setting all VCS repository names: %s', {
220 log.debug('Setting all VCS repository names: %s', {
221 'acl_repo_name': self.acl_repo_name,
221 'acl_repo_name': self.acl_repo_name,
222 'url_repo_name': self.url_repo_name,
222 'url_repo_name': self.url_repo_name,
223 'vcs_repo_name': self.vcs_repo_name,
223 'vcs_repo_name': self.vcs_repo_name,
224 })
224 })
225
225
226 @property
226 @property
227 def scm_app(self):
227 def scm_app(self):
228 custom_implementation = self.config['vcs.scm_app_implementation']
228 custom_implementation = self.config['vcs.scm_app_implementation']
229 if custom_implementation == 'http':
229 if custom_implementation == 'http':
230 log.info('Using HTTP implementation of scm app.')
230 log.info('Using HTTP implementation of scm app.')
231 scm_app_impl = scm_app_http
231 scm_app_impl = scm_app_http
232 else:
232 else:
233 log.info('Using custom implementation of scm_app: "{}"'.format(
233 log.info('Using custom implementation of scm_app: "{}"'.format(
234 custom_implementation))
234 custom_implementation))
235 scm_app_impl = importlib.import_module(custom_implementation)
235 scm_app_impl = importlib.import_module(custom_implementation)
236 return scm_app_impl
236 return scm_app_impl
237
237
238 def _get_by_id(self, repo_name):
238 def _get_by_id(self, repo_name):
239 """
239 """
240 Gets a special pattern _<ID> from clone url and tries to replace it
240 Gets a special pattern _<ID> from clone url and tries to replace it
241 with a repository_name for support of _<ID> non changeable urls
241 with a repository_name for support of _<ID> non changeable urls
242 """
242 """
243
243
244 data = repo_name.split('/')
244 data = repo_name.split('/')
245 if len(data) >= 2:
245 if len(data) >= 2:
246 from rhodecode.model.repo import RepoModel
246 from rhodecode.model.repo import RepoModel
247 by_id_match = RepoModel().get_repo_by_id(repo_name)
247 by_id_match = RepoModel().get_repo_by_id(repo_name)
248 if by_id_match:
248 if by_id_match:
249 data[1] = by_id_match.repo_name
249 data[1] = by_id_match.repo_name
250
250
251 return safe_str('/'.join(data))
251 return safe_str('/'.join(data))
252
252
253 def _invalidate_cache(self, repo_name):
253 def _invalidate_cache(self, repo_name):
254 """
254 """
255 Set's cache for this repository for invalidation on next access
255 Set's cache for this repository for invalidation on next access
256
256
257 :param repo_name: full repo name, also a cache key
257 :param repo_name: full repo name, also a cache key
258 """
258 """
259 ScmModel().mark_for_invalidation(repo_name)
259 ScmModel().mark_for_invalidation(repo_name)
260
260
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 db_repo = Repository.get_by_repo_name(repo_name)
262 db_repo = Repository.get_by_repo_name(repo_name)
263 if not db_repo:
263 if not db_repo:
264 log.debug('Repository `%s` not found inside the database.',
264 log.debug('Repository `%s` not found inside the database.',
265 repo_name)
265 repo_name)
266 return False
266 return False
267
267
268 if db_repo.repo_type != scm_type:
268 if db_repo.repo_type != scm_type:
269 log.warning(
269 log.warning(
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 repo_name, db_repo.repo_type, scm_type)
271 repo_name, db_repo.repo_type, scm_type)
272 return False
272 return False
273
273
274 config = db_repo._config
274 config = db_repo._config
275 config.set('extensions', 'largefiles', '')
275 config.set('extensions', 'largefiles', '')
276 return is_valid_repo(
276 return is_valid_repo(
277 repo_name, base_path,
277 repo_name, base_path,
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279
279
280 def valid_and_active_user(self, user):
280 def valid_and_active_user(self, user):
281 """
281 """
282 Checks if that user is not empty, and if it's actually object it checks
282 Checks if that user is not empty, and if it's actually object it checks
283 if he's active.
283 if he's active.
284
284
285 :param user: user object or None
285 :param user: user object or None
286 :return: boolean
286 :return: boolean
287 """
287 """
288 if user is None:
288 if user is None:
289 return False
289 return False
290
290
291 elif user.active:
291 elif user.active:
292 return True
292 return True
293
293
294 return False
294 return False
295
295
296 @property
296 @property
297 def is_shadow_repo_dir(self):
297 def is_shadow_repo_dir(self):
298 return os.path.isdir(self.vcs_repo_name)
298 return os.path.isdir(self.vcs_repo_name)
299
299
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 """
302 """
303 Checks permissions using action (push/pull) user and repository
303 Checks permissions using action (push/pull) user and repository
304 name. If plugin_cache and ttl is set it will use the plugin which
304 name. If plugin_cache and ttl is set it will use the plugin which
305 authenticated the user to store the cached permissions result for N
305 authenticated the user to store the cached permissions result for N
306 amount of seconds as in cache_ttl
306 amount of seconds as in cache_ttl
307
307
308 :param action: push or pull action
308 :param action: push or pull action
309 :param user: user instance
309 :param user: user instance
310 :param repo_name: repository name
310 :param repo_name: repository name
311 """
311 """
312
312
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 plugin_id, plugin_cache_active, cache_ttl)
314 plugin_id, plugin_cache_active, cache_ttl)
315
315
316 user_id = user.user_id
316 user_id = user.user_id
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319
319
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 expiration_time=cache_ttl,
321 expiration_time=cache_ttl,
322 condition=plugin_cache_active)
322 condition=plugin_cache_active)
323 def compute_perm_vcs(
323 def compute_perm_vcs(
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325
325
326 log.debug('auth: calculating permission access now...')
326 log.debug('auth: calculating permission access now...')
327 # check IP
327 # check IP
328 inherit = user.inherit_default_permissions
328 inherit = user.inherit_default_permissions
329 ip_allowed = AuthUser.check_ip_allowed(
329 ip_allowed = AuthUser.check_ip_allowed(
330 user_id, ip_addr, inherit_from_default=inherit)
330 user_id, ip_addr, inherit_from_default=inherit)
331 if ip_allowed:
331 if ip_allowed:
332 log.info('Access for IP:%s allowed', ip_addr)
332 log.info('Access for IP:%s allowed', ip_addr)
333 else:
333 else:
334 return False
334 return False
335
335
336 if action == 'push':
336 if action == 'push':
337 perms = ('repository.write', 'repository.admin')
337 perms = ('repository.write', 'repository.admin')
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
339 return False
339 return False
340
340
341 else:
341 else:
342 # any other action need at least read permission
342 # any other action need at least read permission
343 perms = (
343 perms = (
344 'repository.read', 'repository.write', 'repository.admin')
344 'repository.read', 'repository.write', 'repository.admin')
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
346 return False
346 return False
347
347
348 return True
348 return True
349
349
350 start = time.time()
350 start = time.time()
351 log.debug('Running plugin `%s` permissions check', plugin_id)
351 log.debug('Running plugin `%s` permissions check', plugin_id)
352
352
353 # for environ based auth, password can be empty, but then the validation is
353 # for environ based auth, password can be empty, but then the validation is
354 # on the server that fills in the env data needed for authentication
354 # on the server that fills in the env data needed for authentication
355 perm_result = compute_perm_vcs(
355 perm_result = compute_perm_vcs(
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357
357
358 auth_time = time.time() - start
358 auth_time = time.time() - start
359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
360 'expiration time of fetched cache %.1fs.',
360 'expiration time of fetched cache %.1fs.',
361 plugin_id, auth_time, cache_ttl)
361 plugin_id, auth_time, cache_ttl)
362
362
363 return perm_result
363 return perm_result
364
364
365 def _check_ssl(self, environ, start_response):
365 def _check_ssl(self, environ, start_response):
366 """
366 """
367 Checks the SSL check flag and returns False if SSL is not present
367 Checks the SSL check flag and returns False if SSL is not present
368 and required True otherwise
368 and required True otherwise
369 """
369 """
370 org_proto = environ['wsgi._org_proto']
370 org_proto = environ['wsgi._org_proto']
371 # check if we have SSL required ! if not it's a bad request !
371 # check if we have SSL required ! if not it's a bad request !
372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
373 if require_ssl and org_proto == 'http':
373 if require_ssl and org_proto == 'http':
374 log.debug(
374 log.debug(
375 'Bad request: detected protocol is `%s` and '
375 'Bad request: detected protocol is `%s` and '
376 'SSL/HTTPS is required.', org_proto)
376 'SSL/HTTPS is required.', org_proto)
377 return False
377 return False
378 return True
378 return True
379
379
380 def _get_default_cache_ttl(self):
380 def _get_default_cache_ttl(self):
381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
383 plugin_settings = plugin.get_settings()
383 plugin_settings = plugin.get_settings()
384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
385 plugin_settings) or (False, 0)
385 plugin_settings) or (False, 0)
386 return plugin_cache_active, cache_ttl
386 return plugin_cache_active, cache_ttl
387
387
388 def __call__(self, environ, start_response):
388 def __call__(self, environ, start_response):
389 try:
389 try:
390 return self._handle_request(environ, start_response)
390 return self._handle_request(environ, start_response)
391 except Exception:
391 except Exception:
392 log.exception("Exception while handling request")
392 log.exception("Exception while handling request")
393 appenlight.track_exception(environ)
393 appenlight.track_exception(environ)
394 return HTTPInternalServerError()(environ, start_response)
394 return HTTPInternalServerError()(environ, start_response)
395 finally:
395 finally:
396 meta.Session.remove()
396 meta.Session.remove()
397
397
398 def _handle_request(self, environ, start_response):
398 def _handle_request(self, environ, start_response):
399
399
400 if not self._check_ssl(environ, start_response):
400 if not self._check_ssl(environ, start_response):
401 reason = ('SSL required, while RhodeCode was unable '
401 reason = ('SSL required, while RhodeCode was unable '
402 'to detect this as SSL request')
402 'to detect this as SSL request')
403 log.debug('User not allowed to proceed, %s', reason)
403 log.debug('User not allowed to proceed, %s', reason)
404 return HTTPNotAcceptable(reason)(environ, start_response)
404 return HTTPNotAcceptable(reason)(environ, start_response)
405
405
406 if not self.url_repo_name:
406 if not self.url_repo_name:
407 log.warning('Repository name is empty: %s', self.url_repo_name)
407 log.warning('Repository name is empty: %s', self.url_repo_name)
408 # failed to get repo name, we fail now
408 # failed to get repo name, we fail now
409 return HTTPNotFound()(environ, start_response)
409 return HTTPNotFound()(environ, start_response)
410 log.debug('Extracted repo name is %s', self.url_repo_name)
410 log.debug('Extracted repo name is %s', self.url_repo_name)
411
411
412 ip_addr = get_ip_addr(environ)
412 ip_addr = get_ip_addr(environ)
413 user_agent = get_user_agent(environ)
413 user_agent = get_user_agent(environ)
414 username = None
414 username = None
415
415
416 # skip passing error to error controller
416 # skip passing error to error controller
417 environ['pylons.status_code_redirect'] = True
417 environ['pylons.status_code_redirect'] = True
418
418
419 # ======================================================================
419 # ======================================================================
420 # GET ACTION PULL or PUSH
420 # GET ACTION PULL or PUSH
421 # ======================================================================
421 # ======================================================================
422 action = self._get_action(environ)
422 action = self._get_action(environ)
423
423
424 # ======================================================================
424 # ======================================================================
425 # Check if this is a request to a shadow repository of a pull request.
425 # Check if this is a request to a shadow repository of a pull request.
426 # In this case only pull action is allowed.
426 # In this case only pull action is allowed.
427 # ======================================================================
427 # ======================================================================
428 if self.is_shadow_repo and action != 'pull':
428 if self.is_shadow_repo and action != 'pull':
429 reason = 'Only pull action is allowed for shadow repositories.'
429 reason = 'Only pull action is allowed for shadow repositories.'
430 log.debug('User not allowed to proceed, %s', reason)
430 log.debug('User not allowed to proceed, %s', reason)
431 return HTTPNotAcceptable(reason)(environ, start_response)
431 return HTTPNotAcceptable(reason)(environ, start_response)
432
432
433 # Check if the shadow repo actually exists, in case someone refers
433 # Check if the shadow repo actually exists, in case someone refers
434 # to it, and it has been deleted because of successful merge.
434 # to it, and it has been deleted because of successful merge.
435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
436 log.debug(
436 log.debug(
437 'Shadow repo detected, and shadow repo dir `%s` is missing',
437 'Shadow repo detected, and shadow repo dir `%s` is missing',
438 self.is_shadow_repo_dir)
438 self.is_shadow_repo_dir)
439 return HTTPNotFound()(environ, start_response)
439 return HTTPNotFound()(environ, start_response)
440
440
441 # ======================================================================
441 # ======================================================================
442 # CHECK ANONYMOUS PERMISSION
442 # CHECK ANONYMOUS PERMISSION
443 # ======================================================================
443 # ======================================================================
444 if action in ['pull', 'push']:
444 if action in ['pull', 'push']:
445 anonymous_user = User.get_default_user()
445 anonymous_user = User.get_default_user()
446 username = anonymous_user.username
446 username = anonymous_user.username
447 if anonymous_user.active:
447 if anonymous_user.active:
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
449 # ONLY check permissions if the user is activated
449 # ONLY check permissions if the user is activated
450 anonymous_perm = self._check_permission(
450 anonymous_perm = self._check_permission(
451 action, anonymous_user, self.acl_repo_name, ip_addr,
451 action, anonymous_user, self.acl_repo_name, ip_addr,
452 plugin_id='anonymous_access',
452 plugin_id='anonymous_access',
453 plugin_cache_active=plugin_cache_active,
453 plugin_cache_active=plugin_cache_active,
454 cache_ttl=cache_ttl,
454 cache_ttl=cache_ttl,
455 )
455 )
456 else:
456 else:
457 anonymous_perm = False
457 anonymous_perm = False
458
458
459 if not anonymous_user.active or not anonymous_perm:
459 if not anonymous_user.active or not anonymous_perm:
460 if not anonymous_user.active:
460 if not anonymous_user.active:
461 log.debug('Anonymous access is disabled, running '
461 log.debug('Anonymous access is disabled, running '
462 'authentication')
462 'authentication')
463
463
464 if not anonymous_perm:
464 if not anonymous_perm:
465 log.debug('Not enough credentials to access this '
465 log.debug('Not enough credentials to access this '
466 'repository as anonymous user')
466 'repository as anonymous user')
467
467
468 username = None
468 username = None
469 # ==============================================================
469 # ==============================================================
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
472 # ==============================================================
472 # ==============================================================
473
473
474 # try to auth based on environ, container auth methods
474 # try to auth based on environ, container auth methods
475 log.debug('Running PRE-AUTH for container based authentication')
475 log.debug('Running PRE-AUTH for container based authentication')
476 pre_auth = authenticate(
476 pre_auth = authenticate(
477 '', '', environ, VCS_TYPE, registry=self.registry,
477 '', '', environ, VCS_TYPE, registry=self.registry,
478 acl_repo_name=self.acl_repo_name)
478 acl_repo_name=self.acl_repo_name)
479 if pre_auth and pre_auth.get('username'):
479 if pre_auth and pre_auth.get('username'):
480 username = pre_auth['username']
480 username = pre_auth['username']
481 log.debug('PRE-AUTH got %s as username', username)
481 log.debug('PRE-AUTH got %s as username', username)
482 if pre_auth:
482 if pre_auth:
483 log.debug('PRE-AUTH successful from %s',
483 log.debug('PRE-AUTH successful from %s',
484 pre_auth.get('auth_data', {}).get('_plugin'))
484 pre_auth.get('auth_data', {}).get('_plugin'))
485
485
486 # If not authenticated by the container, running basic auth
486 # If not authenticated by the container, running basic auth
487 # before inject the calling repo_name for special scope checks
487 # before inject the calling repo_name for special scope checks
488 self.authenticate.acl_repo_name = self.acl_repo_name
488 self.authenticate.acl_repo_name = self.acl_repo_name
489
489
490 plugin_cache_active, cache_ttl = False, 0
490 plugin_cache_active, cache_ttl = False, 0
491 plugin = None
491 plugin = None
492 if not username:
492 if not username:
493 self.authenticate.realm = self.authenticate.get_rc_realm()
493 self.authenticate.realm = self.authenticate.get_rc_realm()
494
494
495 try:
495 try:
496 auth_result = self.authenticate(environ)
496 auth_result = self.authenticate(environ)
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
498 log.error(e)
498 log.error(e)
499 reason = safe_str(e)
499 reason = safe_str(e)
500 return HTTPNotAcceptable(reason)(environ, start_response)
500 return HTTPNotAcceptable(reason)(environ, start_response)
501
501
502 if isinstance(auth_result, dict):
502 if isinstance(auth_result, dict):
503 AUTH_TYPE.update(environ, 'basic')
503 AUTH_TYPE.update(environ, 'basic')
504 REMOTE_USER.update(environ, auth_result['username'])
504 REMOTE_USER.update(environ, auth_result['username'])
505 username = auth_result['username']
505 username = auth_result['username']
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
507 log.info(
507 log.info(
508 'MAIN-AUTH successful for user `%s` from %s plugin',
508 'MAIN-AUTH successful for user `%s` from %s plugin',
509 username, plugin)
509 username, plugin)
510
510
511 plugin_cache_active, cache_ttl = auth_result.get(
511 plugin_cache_active, cache_ttl = auth_result.get(
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
513 else:
513 else:
514 return auth_result.wsgi_application(
514 return auth_result.wsgi_application(
515 environ, start_response)
515 environ, start_response)
516
516
517 # ==============================================================
517 # ==============================================================
518 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
518 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
519 # ==============================================================
519 # ==============================================================
520 user = User.get_by_username(username)
520 user = User.get_by_username(username)
521 if not self.valid_and_active_user(user):
521 if not self.valid_and_active_user(user):
522 return HTTPForbidden()(environ, start_response)
522 return HTTPForbidden()(environ, start_response)
523 username = user.username
523 username = user.username
524 user_id = user.user_id
524
525
525 # check user attributes for password change flag
526 # check user attributes for password change flag
526 user_obj = user
527 user_obj = user
527 if user_obj and user_obj.username != User.DEFAULT_USER and \
528 if user_obj and user_obj.username != User.DEFAULT_USER and \
528 user_obj.user_data.get('force_password_change'):
529 user_obj.user_data.get('force_password_change'):
529 reason = 'password change required'
530 reason = 'password change required'
530 log.debug('User not allowed to authenticate, %s', reason)
531 log.debug('User not allowed to authenticate, %s', reason)
531 return HTTPNotAcceptable(reason)(environ, start_response)
532 return HTTPNotAcceptable(reason)(environ, start_response)
532
533
533 # check permissions for this repository
534 # check permissions for this repository
534 perm = self._check_permission(
535 perm = self._check_permission(
535 action, user, self.acl_repo_name, ip_addr,
536 action, user, self.acl_repo_name, ip_addr,
536 plugin, plugin_cache_active, cache_ttl)
537 plugin, plugin_cache_active, cache_ttl)
537 if not perm:
538 if not perm:
538 return HTTPForbidden()(environ, start_response)
539 return HTTPForbidden()(environ, start_response)
540 environ['rc_auth_user_id'] = user_id
539
541
540 # extras are injected into UI object and later available
542 # extras are injected into UI object and later available
541 # in hooks executed by RhodeCode
543 # in hooks executed by RhodeCode
542 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
544 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
543 extras = vcs_operation_context(
545 extras = vcs_operation_context(
544 environ, repo_name=self.acl_repo_name, username=username,
546 environ, repo_name=self.acl_repo_name, username=username,
545 action=action, scm=self.SCM, check_locking=check_locking,
547 action=action, scm=self.SCM, check_locking=check_locking,
546 is_shadow_repo=self.is_shadow_repo
548 is_shadow_repo=self.is_shadow_repo
547 )
549 )
548
550
549 # ======================================================================
551 # ======================================================================
550 # REQUEST HANDLING
552 # REQUEST HANDLING
551 # ======================================================================
553 # ======================================================================
552 repo_path = os.path.join(
554 repo_path = os.path.join(
553 safe_str(self.base_path), safe_str(self.vcs_repo_name))
555 safe_str(self.base_path), safe_str(self.vcs_repo_name))
554 log.debug('Repository path is %s', repo_path)
556 log.debug('Repository path is %s', repo_path)
555
557
556 fix_PATH()
558 fix_PATH()
557
559
558 log.info(
560 log.info(
559 '%s action on %s repo "%s" by "%s" from %s %s',
561 '%s action on %s repo "%s" by "%s" from %s %s',
560 action, self.SCM, safe_str(self.url_repo_name),
562 action, self.SCM, safe_str(self.url_repo_name),
561 safe_str(username), ip_addr, user_agent)
563 safe_str(username), ip_addr, user_agent)
562
564
563 return self._generate_vcs_response(
565 return self._generate_vcs_response(
564 environ, start_response, repo_path, extras, action)
566 environ, start_response, repo_path, extras, action)
565
567
566 @initialize_generator
568 @initialize_generator
567 def _generate_vcs_response(
569 def _generate_vcs_response(
568 self, environ, start_response, repo_path, extras, action):
570 self, environ, start_response, repo_path, extras, action):
569 """
571 """
570 Returns a generator for the response content.
572 Returns a generator for the response content.
571
573
572 This method is implemented as a generator, so that it can trigger
574 This method is implemented as a generator, so that it can trigger
573 the cache validation after all content sent back to the client. It
575 the cache validation after all content sent back to the client. It
574 also handles the locking exceptions which will be triggered when
576 also handles the locking exceptions which will be triggered when
575 the first chunk is produced by the underlying WSGI application.
577 the first chunk is produced by the underlying WSGI application.
576 """
578 """
577 txn_id = ''
579 txn_id = ''
578 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
580 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
579 # case for SVN, we want to re-use the callback daemon port
581 # case for SVN, we want to re-use the callback daemon port
580 # so we use the txn_id, for this we peek the body, and still save
582 # so we use the txn_id, for this we peek the body, and still save
581 # it as wsgi.input
583 # it as wsgi.input
582 data = environ['wsgi.input'].read()
584 data = environ['wsgi.input'].read()
583 environ['wsgi.input'] = StringIO(data)
585 environ['wsgi.input'] = StringIO(data)
584 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
586 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
585
587
586 callback_daemon, extras = self._prepare_callback_daemon(
588 callback_daemon, extras = self._prepare_callback_daemon(
587 extras, environ, action, txn_id=txn_id)
589 extras, environ, action, txn_id=txn_id)
588 log.debug('HOOKS extras is %s', extras)
590 log.debug('HOOKS extras is %s', extras)
589
591
590 config = self._create_config(extras, self.acl_repo_name)
592 config = self._create_config(extras, self.acl_repo_name)
591 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
593 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
592 with callback_daemon:
594 with callback_daemon:
593 app.rc_extras = extras
595 app.rc_extras = extras
594
596
595 try:
597 try:
596 response = app(environ, start_response)
598 response = app(environ, start_response)
597 finally:
599 finally:
598 # This statement works together with the decorator
600 # This statement works together with the decorator
599 # "initialize_generator" above. The decorator ensures that
601 # "initialize_generator" above. The decorator ensures that
600 # we hit the first yield statement before the generator is
602 # we hit the first yield statement before the generator is
601 # returned back to the WSGI server. This is needed to
603 # returned back to the WSGI server. This is needed to
602 # ensure that the call to "app" above triggers the
604 # ensure that the call to "app" above triggers the
603 # needed callback to "start_response" before the
605 # needed callback to "start_response" before the
604 # generator is actually used.
606 # generator is actually used.
605 yield "__init__"
607 yield "__init__"
606
608
607 # iter content
609 # iter content
608 for chunk in response:
610 for chunk in response:
609 yield chunk
611 yield chunk
610
612
611 try:
613 try:
612 # invalidate cache on push
614 # invalidate cache on push
613 if action == 'push':
615 if action == 'push':
614 self._invalidate_cache(self.url_repo_name)
616 self._invalidate_cache(self.url_repo_name)
615 finally:
617 finally:
616 meta.Session.remove()
618 meta.Session.remove()
617
619
618 def _get_repository_name(self, environ):
620 def _get_repository_name(self, environ):
619 """Get repository name out of the environmnent
621 """Get repository name out of the environmnent
620
622
621 :param environ: WSGI environment
623 :param environ: WSGI environment
622 """
624 """
623 raise NotImplementedError()
625 raise NotImplementedError()
624
626
625 def _get_action(self, environ):
627 def _get_action(self, environ):
626 """Map request commands into a pull or push command.
628 """Map request commands into a pull or push command.
627
629
628 :param environ: WSGI environment
630 :param environ: WSGI environment
629 """
631 """
630 raise NotImplementedError()
632 raise NotImplementedError()
631
633
632 def _create_wsgi_app(self, repo_path, repo_name, config):
634 def _create_wsgi_app(self, repo_path, repo_name, config):
633 """Return the WSGI app that will finally handle the request."""
635 """Return the WSGI app that will finally handle the request."""
634 raise NotImplementedError()
636 raise NotImplementedError()
635
637
636 def _create_config(self, extras, repo_name):
638 def _create_config(self, extras, repo_name):
637 """Create a safe config representation."""
639 """Create a safe config representation."""
638 raise NotImplementedError()
640 raise NotImplementedError()
639
641
640 def _should_use_callback_daemon(self, extras, environ, action):
642 def _should_use_callback_daemon(self, extras, environ, action):
641 return True
643 return True
642
644
643 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
644 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
646 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
645 if not self._should_use_callback_daemon(extras, environ, action):
647 if not self._should_use_callback_daemon(extras, environ, action):
646 # disable callback daemon for actions that don't require it
648 # disable callback daemon for actions that don't require it
647 direct_calls = True
649 direct_calls = True
648
650
649 return prepare_callback_daemon(
651 return prepare_callback_daemon(
650 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
652 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
651 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
653 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
652
654
653
655
654 def _should_check_locking(query_string):
656 def _should_check_locking(query_string):
655 # this is kind of hacky, but due to how mercurial handles client-server
657 # this is kind of hacky, but due to how mercurial handles client-server
656 # server see all operation on commit; bookmarks, phases and
658 # server see all operation on commit; bookmarks, phases and
657 # obsolescence marker in different transaction, we don't want to check
659 # obsolescence marker in different transaction, we don't want to check
658 # locking on those
660 # locking on those
659 return query_string not in ['cmd=listkeys']
661 return query_string not in ['cmd=listkeys']
@@ -1,4540 +1,4534 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37 from sqlalchemy import (
37 from sqlalchemy import (
38 or_, and_, not_, func, TypeDecorator, event,
38 or_, and_, not_, func, TypeDecorator, event,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType)
41 Text, Float, PickleType)
42 from sqlalchemy.sql.expression import true, false
42 from sqlalchemy.sql.expression import true, false
43 from sqlalchemy.sql.functions import coalesce, count # noqa
43 from sqlalchemy.sql.functions import coalesce, count # noqa
44 from sqlalchemy.orm import (
44 from sqlalchemy.orm import (
45 relationship, joinedload, class_mapper, validates, aliased)
45 relationship, joinedload, class_mapper, validates, aliased)
46 from sqlalchemy.ext.declarative import declared_attr
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # noqa
48 from sqlalchemy.exc import IntegrityError # noqa
49 from sqlalchemy.dialects.mysql import LONGTEXT
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from beaker.cache import cache_region
50 from beaker.cache import cache_region
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
52
52
53 from pyramid.threadlocal import get_current_request
53 from pyramid.threadlocal import get_current_request
54
54
55 from rhodecode.translation import _
55 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance
56 from rhodecode.lib.vcs import get_vcs_instance
57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
58 from rhodecode.lib.utils2 import (
58 from rhodecode.lib.utils2 import (
59 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
59 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re, StrictAttributeDict, cleaned_uri)
61 glob2re, StrictAttributeDict, cleaned_uri)
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
63 JsonRaw
63 JsonRaw
64 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.ext_json import json
65 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.caching_query import FromCache
66 from rhodecode.lib.encrypt import AESCipher
66 from rhodecode.lib.encrypt import AESCipher
67
67
68 from rhodecode.model.meta import Base, Session
68 from rhodecode.model.meta import Base, Session
69
69
70 URL_SEP = '/'
70 URL_SEP = '/'
71 log = logging.getLogger(__name__)
71 log = logging.getLogger(__name__)
72
72
73 # =============================================================================
73 # =============================================================================
74 # BASE CLASSES
74 # BASE CLASSES
75 # =============================================================================
75 # =============================================================================
76
76
77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
78 # beaker.session.secret if first is not set.
78 # beaker.session.secret if first is not set.
79 # and initialized at environment.py
79 # and initialized at environment.py
80 ENCRYPTION_KEY = None
80 ENCRYPTION_KEY = None
81
81
82 # used to sort permissions by types, '#' used here is not allowed to be in
82 # used to sort permissions by types, '#' used here is not allowed to be in
83 # usernames, and it's very early in sorted string.printable table.
83 # usernames, and it's very early in sorted string.printable table.
84 PERMISSION_TYPE_SORT = {
84 PERMISSION_TYPE_SORT = {
85 'admin': '####',
85 'admin': '####',
86 'write': '###',
86 'write': '###',
87 'read': '##',
87 'read': '##',
88 'none': '#',
88 'none': '#',
89 }
89 }
90
90
91
91
92 def display_user_sort(obj):
92 def display_user_sort(obj):
93 """
93 """
94 Sort function used to sort permissions in .permissions() function of
94 Sort function used to sort permissions in .permissions() function of
95 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 Repository, RepoGroup, UserGroup. Also it put the default user in front
96 of all other resources
96 of all other resources
97 """
97 """
98
98
99 if obj.username == User.DEFAULT_USER:
99 if obj.username == User.DEFAULT_USER:
100 return '#####'
100 return '#####'
101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
102 return prefix + obj.username
102 return prefix + obj.username
103
103
104
104
105 def display_user_group_sort(obj):
105 def display_user_group_sort(obj):
106 """
106 """
107 Sort function used to sort permissions in .permissions() function of
107 Sort function used to sort permissions in .permissions() function of
108 Repository, RepoGroup, UserGroup. Also it put the default user in front
108 Repository, RepoGroup, UserGroup. Also it put the default user in front
109 of all other resources
109 of all other resources
110 """
110 """
111
111
112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
113 return prefix + obj.users_group_name
113 return prefix + obj.users_group_name
114
114
115
115
116 def _hash_key(k):
116 def _hash_key(k):
117 return sha1_safe(k)
117 return sha1_safe(k)
118
118
119
119
120 def in_filter_generator(qry, items, limit=500):
120 def in_filter_generator(qry, items, limit=500):
121 """
121 """
122 Splits IN() into multiple with OR
122 Splits IN() into multiple with OR
123 e.g.::
123 e.g.::
124 cnt = Repository.query().filter(
124 cnt = Repository.query().filter(
125 or_(
125 or_(
126 *in_filter_generator(Repository.repo_id, range(100000))
126 *in_filter_generator(Repository.repo_id, range(100000))
127 )).count()
127 )).count()
128 """
128 """
129 if not items:
129 if not items:
130 # empty list will cause empty query which might cause security issues
130 # empty list will cause empty query which might cause security issues
131 # this can lead to hidden unpleasant results
131 # this can lead to hidden unpleasant results
132 items = [-1]
132 items = [-1]
133
133
134 parts = []
134 parts = []
135 for chunk in xrange(0, len(items), limit):
135 for chunk in xrange(0, len(items), limit):
136 parts.append(
136 parts.append(
137 qry.in_(items[chunk: chunk + limit])
137 qry.in_(items[chunk: chunk + limit])
138 )
138 )
139
139
140 return parts
140 return parts
141
141
142
142
143 base_table_args = {
143 base_table_args = {
144 'extend_existing': True,
144 'extend_existing': True,
145 'mysql_engine': 'InnoDB',
145 'mysql_engine': 'InnoDB',
146 'mysql_charset': 'utf8',
146 'mysql_charset': 'utf8',
147 'sqlite_autoincrement': True
147 'sqlite_autoincrement': True
148 }
148 }
149
149
150
150
151 class EncryptedTextValue(TypeDecorator):
151 class EncryptedTextValue(TypeDecorator):
152 """
152 """
153 Special column for encrypted long text data, use like::
153 Special column for encrypted long text data, use like::
154
154
155 value = Column("encrypted_value", EncryptedValue(), nullable=False)
155 value = Column("encrypted_value", EncryptedValue(), nullable=False)
156
156
157 This column is intelligent so if value is in unencrypted form it return
157 This column is intelligent so if value is in unencrypted form it return
158 unencrypted form, but on save it always encrypts
158 unencrypted form, but on save it always encrypts
159 """
159 """
160 impl = Text
160 impl = Text
161
161
162 def process_bind_param(self, value, dialect):
162 def process_bind_param(self, value, dialect):
163 if not value:
163 if not value:
164 return value
164 return value
165 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
165 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
166 # protect against double encrypting if someone manually starts
166 # protect against double encrypting if someone manually starts
167 # doing
167 # doing
168 raise ValueError('value needs to be in unencrypted format, ie. '
168 raise ValueError('value needs to be in unencrypted format, ie. '
169 'not starting with enc$aes')
169 'not starting with enc$aes')
170 return 'enc$aes_hmac$%s' % AESCipher(
170 return 'enc$aes_hmac$%s' % AESCipher(
171 ENCRYPTION_KEY, hmac=True).encrypt(value)
171 ENCRYPTION_KEY, hmac=True).encrypt(value)
172
172
173 def process_result_value(self, value, dialect):
173 def process_result_value(self, value, dialect):
174 import rhodecode
174 import rhodecode
175
175
176 if not value:
176 if not value:
177 return value
177 return value
178
178
179 parts = value.split('$', 3)
179 parts = value.split('$', 3)
180 if not len(parts) == 3:
180 if not len(parts) == 3:
181 # probably not encrypted values
181 # probably not encrypted values
182 return value
182 return value
183 else:
183 else:
184 if parts[0] != 'enc':
184 if parts[0] != 'enc':
185 # parts ok but without our header ?
185 # parts ok but without our header ?
186 return value
186 return value
187 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
187 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
188 'rhodecode.encrypted_values.strict') or True)
188 'rhodecode.encrypted_values.strict') or True)
189 # at that stage we know it's our encryption
189 # at that stage we know it's our encryption
190 if parts[1] == 'aes':
190 if parts[1] == 'aes':
191 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
191 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
192 elif parts[1] == 'aes_hmac':
192 elif parts[1] == 'aes_hmac':
193 decrypted_data = AESCipher(
193 decrypted_data = AESCipher(
194 ENCRYPTION_KEY, hmac=True,
194 ENCRYPTION_KEY, hmac=True,
195 strict_verification=enc_strict_mode).decrypt(parts[2])
195 strict_verification=enc_strict_mode).decrypt(parts[2])
196 else:
196 else:
197 raise ValueError(
197 raise ValueError(
198 'Encryption type part is wrong, must be `aes` '
198 'Encryption type part is wrong, must be `aes` '
199 'or `aes_hmac`, got `%s` instead' % (parts[1]))
199 'or `aes_hmac`, got `%s` instead' % (parts[1]))
200 return decrypted_data
200 return decrypted_data
201
201
202
202
203 class BaseModel(object):
203 class BaseModel(object):
204 """
204 """
205 Base Model for all classes
205 Base Model for all classes
206 """
206 """
207
207
208 @classmethod
208 @classmethod
209 def _get_keys(cls):
209 def _get_keys(cls):
210 """return column names for this model """
210 """return column names for this model """
211 return class_mapper(cls).c.keys()
211 return class_mapper(cls).c.keys()
212
212
213 def get_dict(self):
213 def get_dict(self):
214 """
214 """
215 return dict with keys and values corresponding
215 return dict with keys and values corresponding
216 to this model data """
216 to this model data """
217
217
218 d = {}
218 d = {}
219 for k in self._get_keys():
219 for k in self._get_keys():
220 d[k] = getattr(self, k)
220 d[k] = getattr(self, k)
221
221
222 # also use __json__() if present to get additional fields
222 # also use __json__() if present to get additional fields
223 _json_attr = getattr(self, '__json__', None)
223 _json_attr = getattr(self, '__json__', None)
224 if _json_attr:
224 if _json_attr:
225 # update with attributes from __json__
225 # update with attributes from __json__
226 if callable(_json_attr):
226 if callable(_json_attr):
227 _json_attr = _json_attr()
227 _json_attr = _json_attr()
228 for k, val in _json_attr.iteritems():
228 for k, val in _json_attr.iteritems():
229 d[k] = val
229 d[k] = val
230 return d
230 return d
231
231
232 def get_appstruct(self):
232 def get_appstruct(self):
233 """return list with keys and values tuples corresponding
233 """return list with keys and values tuples corresponding
234 to this model data """
234 to this model data """
235
235
236 lst = []
236 lst = []
237 for k in self._get_keys():
237 for k in self._get_keys():
238 lst.append((k, getattr(self, k),))
238 lst.append((k, getattr(self, k),))
239 return lst
239 return lst
240
240
241 def populate_obj(self, populate_dict):
241 def populate_obj(self, populate_dict):
242 """populate model with data from given populate_dict"""
242 """populate model with data from given populate_dict"""
243
243
244 for k in self._get_keys():
244 for k in self._get_keys():
245 if k in populate_dict:
245 if k in populate_dict:
246 setattr(self, k, populate_dict[k])
246 setattr(self, k, populate_dict[k])
247
247
248 @classmethod
248 @classmethod
249 def query(cls):
249 def query(cls):
250 return Session().query(cls)
250 return Session().query(cls)
251
251
252 @classmethod
252 @classmethod
253 def get(cls, id_):
253 def get(cls, id_):
254 if id_:
254 if id_:
255 return cls.query().get(id_)
255 return cls.query().get(id_)
256
256
257 @classmethod
257 @classmethod
258 def get_or_404(cls, id_):
258 def get_or_404(cls, id_):
259 from pyramid.httpexceptions import HTTPNotFound
259 from pyramid.httpexceptions import HTTPNotFound
260
260
261 try:
261 try:
262 id_ = int(id_)
262 id_ = int(id_)
263 except (TypeError, ValueError):
263 except (TypeError, ValueError):
264 raise HTTPNotFound()
264 raise HTTPNotFound()
265
265
266 res = cls.query().get(id_)
266 res = cls.query().get(id_)
267 if not res:
267 if not res:
268 raise HTTPNotFound()
268 raise HTTPNotFound()
269 return res
269 return res
270
270
271 @classmethod
271 @classmethod
272 def getAll(cls):
272 def getAll(cls):
273 # deprecated and left for backward compatibility
273 # deprecated and left for backward compatibility
274 return cls.get_all()
274 return cls.get_all()
275
275
276 @classmethod
276 @classmethod
277 def get_all(cls):
277 def get_all(cls):
278 return cls.query().all()
278 return cls.query().all()
279
279
280 @classmethod
280 @classmethod
281 def delete(cls, id_):
281 def delete(cls, id_):
282 obj = cls.query().get(id_)
282 obj = cls.query().get(id_)
283 Session().delete(obj)
283 Session().delete(obj)
284
284
285 @classmethod
285 @classmethod
286 def identity_cache(cls, session, attr_name, value):
286 def identity_cache(cls, session, attr_name, value):
287 exist_in_session = []
287 exist_in_session = []
288 for (item_cls, pkey), instance in session.identity_map.items():
288 for (item_cls, pkey), instance in session.identity_map.items():
289 if cls == item_cls and getattr(instance, attr_name) == value:
289 if cls == item_cls and getattr(instance, attr_name) == value:
290 exist_in_session.append(instance)
290 exist_in_session.append(instance)
291 if exist_in_session:
291 if exist_in_session:
292 if len(exist_in_session) == 1:
292 if len(exist_in_session) == 1:
293 return exist_in_session[0]
293 return exist_in_session[0]
294 log.exception(
294 log.exception(
295 'multiple objects with attr %s and '
295 'multiple objects with attr %s and '
296 'value %s found with same name: %r',
296 'value %s found with same name: %r',
297 attr_name, value, exist_in_session)
297 attr_name, value, exist_in_session)
298
298
299 def __repr__(self):
299 def __repr__(self):
300 if hasattr(self, '__unicode__'):
300 if hasattr(self, '__unicode__'):
301 # python repr needs to return str
301 # python repr needs to return str
302 try:
302 try:
303 return safe_str(self.__unicode__())
303 return safe_str(self.__unicode__())
304 except UnicodeDecodeError:
304 except UnicodeDecodeError:
305 pass
305 pass
306 return '<DB:%s>' % (self.__class__.__name__)
306 return '<DB:%s>' % (self.__class__.__name__)
307
307
308
308
309 class RhodeCodeSetting(Base, BaseModel):
309 class RhodeCodeSetting(Base, BaseModel):
310 __tablename__ = 'rhodecode_settings'
310 __tablename__ = 'rhodecode_settings'
311 __table_args__ = (
311 __table_args__ = (
312 UniqueConstraint('app_settings_name'),
312 UniqueConstraint('app_settings_name'),
313 base_table_args
313 base_table_args
314 )
314 )
315
315
316 SETTINGS_TYPES = {
316 SETTINGS_TYPES = {
317 'str': safe_str,
317 'str': safe_str,
318 'int': safe_int,
318 'int': safe_int,
319 'unicode': safe_unicode,
319 'unicode': safe_unicode,
320 'bool': str2bool,
320 'bool': str2bool,
321 'list': functools.partial(aslist, sep=',')
321 'list': functools.partial(aslist, sep=',')
322 }
322 }
323 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
323 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
324 GLOBAL_CONF_KEY = 'app_settings'
324 GLOBAL_CONF_KEY = 'app_settings'
325
325
326 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
326 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
327 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
327 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
328 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
328 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
329 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
329 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
330
330
331 def __init__(self, key='', val='', type='unicode'):
331 def __init__(self, key='', val='', type='unicode'):
332 self.app_settings_name = key
332 self.app_settings_name = key
333 self.app_settings_type = type
333 self.app_settings_type = type
334 self.app_settings_value = val
334 self.app_settings_value = val
335
335
336 @validates('_app_settings_value')
336 @validates('_app_settings_value')
337 def validate_settings_value(self, key, val):
337 def validate_settings_value(self, key, val):
338 assert type(val) == unicode
338 assert type(val) == unicode
339 return val
339 return val
340
340
341 @hybrid_property
341 @hybrid_property
342 def app_settings_value(self):
342 def app_settings_value(self):
343 v = self._app_settings_value
343 v = self._app_settings_value
344 _type = self.app_settings_type
344 _type = self.app_settings_type
345 if _type:
345 if _type:
346 _type = self.app_settings_type.split('.')[0]
346 _type = self.app_settings_type.split('.')[0]
347 # decode the encrypted value
347 # decode the encrypted value
348 if 'encrypted' in self.app_settings_type:
348 if 'encrypted' in self.app_settings_type:
349 cipher = EncryptedTextValue()
349 cipher = EncryptedTextValue()
350 v = safe_unicode(cipher.process_result_value(v, None))
350 v = safe_unicode(cipher.process_result_value(v, None))
351
351
352 converter = self.SETTINGS_TYPES.get(_type) or \
352 converter = self.SETTINGS_TYPES.get(_type) or \
353 self.SETTINGS_TYPES['unicode']
353 self.SETTINGS_TYPES['unicode']
354 return converter(v)
354 return converter(v)
355
355
356 @app_settings_value.setter
356 @app_settings_value.setter
357 def app_settings_value(self, val):
357 def app_settings_value(self, val):
358 """
358 """
359 Setter that will always make sure we use unicode in app_settings_value
359 Setter that will always make sure we use unicode in app_settings_value
360
360
361 :param val:
361 :param val:
362 """
362 """
363 val = safe_unicode(val)
363 val = safe_unicode(val)
364 # encode the encrypted value
364 # encode the encrypted value
365 if 'encrypted' in self.app_settings_type:
365 if 'encrypted' in self.app_settings_type:
366 cipher = EncryptedTextValue()
366 cipher = EncryptedTextValue()
367 val = safe_unicode(cipher.process_bind_param(val, None))
367 val = safe_unicode(cipher.process_bind_param(val, None))
368 self._app_settings_value = val
368 self._app_settings_value = val
369
369
370 @hybrid_property
370 @hybrid_property
371 def app_settings_type(self):
371 def app_settings_type(self):
372 return self._app_settings_type
372 return self._app_settings_type
373
373
374 @app_settings_type.setter
374 @app_settings_type.setter
375 def app_settings_type(self, val):
375 def app_settings_type(self, val):
376 if val.split('.')[0] not in self.SETTINGS_TYPES:
376 if val.split('.')[0] not in self.SETTINGS_TYPES:
377 raise Exception('type must be one of %s got %s'
377 raise Exception('type must be one of %s got %s'
378 % (self.SETTINGS_TYPES.keys(), val))
378 % (self.SETTINGS_TYPES.keys(), val))
379 self._app_settings_type = val
379 self._app_settings_type = val
380
380
381 def __unicode__(self):
381 def __unicode__(self):
382 return u"<%s('%s:%s[%s]')>" % (
382 return u"<%s('%s:%s[%s]')>" % (
383 self.__class__.__name__,
383 self.__class__.__name__,
384 self.app_settings_name, self.app_settings_value,
384 self.app_settings_name, self.app_settings_value,
385 self.app_settings_type
385 self.app_settings_type
386 )
386 )
387
387
388
388
389 class RhodeCodeUi(Base, BaseModel):
389 class RhodeCodeUi(Base, BaseModel):
390 __tablename__ = 'rhodecode_ui'
390 __tablename__ = 'rhodecode_ui'
391 __table_args__ = (
391 __table_args__ = (
392 UniqueConstraint('ui_key'),
392 UniqueConstraint('ui_key'),
393 base_table_args
393 base_table_args
394 )
394 )
395
395
396 HOOK_REPO_SIZE = 'changegroup.repo_size'
396 HOOK_REPO_SIZE = 'changegroup.repo_size'
397 # HG
397 # HG
398 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
398 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
399 HOOK_PULL = 'outgoing.pull_logger'
399 HOOK_PULL = 'outgoing.pull_logger'
400 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
400 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
401 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
401 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
402 HOOK_PUSH = 'changegroup.push_logger'
402 HOOK_PUSH = 'changegroup.push_logger'
403 HOOK_PUSH_KEY = 'pushkey.key_push'
403 HOOK_PUSH_KEY = 'pushkey.key_push'
404
404
405 # TODO: johbo: Unify way how hooks are configured for git and hg,
405 # TODO: johbo: Unify way how hooks are configured for git and hg,
406 # git part is currently hardcoded.
406 # git part is currently hardcoded.
407
407
408 # SVN PATTERNS
408 # SVN PATTERNS
409 SVN_BRANCH_ID = 'vcs_svn_branch'
409 SVN_BRANCH_ID = 'vcs_svn_branch'
410 SVN_TAG_ID = 'vcs_svn_tag'
410 SVN_TAG_ID = 'vcs_svn_tag'
411
411
412 ui_id = Column(
412 ui_id = Column(
413 "ui_id", Integer(), nullable=False, unique=True, default=None,
413 "ui_id", Integer(), nullable=False, unique=True, default=None,
414 primary_key=True)
414 primary_key=True)
415 ui_section = Column(
415 ui_section = Column(
416 "ui_section", String(255), nullable=True, unique=None, default=None)
416 "ui_section", String(255), nullable=True, unique=None, default=None)
417 ui_key = Column(
417 ui_key = Column(
418 "ui_key", String(255), nullable=True, unique=None, default=None)
418 "ui_key", String(255), nullable=True, unique=None, default=None)
419 ui_value = Column(
419 ui_value = Column(
420 "ui_value", String(255), nullable=True, unique=None, default=None)
420 "ui_value", String(255), nullable=True, unique=None, default=None)
421 ui_active = Column(
421 ui_active = Column(
422 "ui_active", Boolean(), nullable=True, unique=None, default=True)
422 "ui_active", Boolean(), nullable=True, unique=None, default=True)
423
423
424 def __repr__(self):
424 def __repr__(self):
425 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
425 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
426 self.ui_key, self.ui_value)
426 self.ui_key, self.ui_value)
427
427
428
428
429 class RepoRhodeCodeSetting(Base, BaseModel):
429 class RepoRhodeCodeSetting(Base, BaseModel):
430 __tablename__ = 'repo_rhodecode_settings'
430 __tablename__ = 'repo_rhodecode_settings'
431 __table_args__ = (
431 __table_args__ = (
432 UniqueConstraint(
432 UniqueConstraint(
433 'app_settings_name', 'repository_id',
433 'app_settings_name', 'repository_id',
434 name='uq_repo_rhodecode_setting_name_repo_id'),
434 name='uq_repo_rhodecode_setting_name_repo_id'),
435 base_table_args
435 base_table_args
436 )
436 )
437
437
438 repository_id = Column(
438 repository_id = Column(
439 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
439 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
440 nullable=False)
440 nullable=False)
441 app_settings_id = Column(
441 app_settings_id = Column(
442 "app_settings_id", Integer(), nullable=False, unique=True,
442 "app_settings_id", Integer(), nullable=False, unique=True,
443 default=None, primary_key=True)
443 default=None, primary_key=True)
444 app_settings_name = Column(
444 app_settings_name = Column(
445 "app_settings_name", String(255), nullable=True, unique=None,
445 "app_settings_name", String(255), nullable=True, unique=None,
446 default=None)
446 default=None)
447 _app_settings_value = Column(
447 _app_settings_value = Column(
448 "app_settings_value", String(4096), nullable=True, unique=None,
448 "app_settings_value", String(4096), nullable=True, unique=None,
449 default=None)
449 default=None)
450 _app_settings_type = Column(
450 _app_settings_type = Column(
451 "app_settings_type", String(255), nullable=True, unique=None,
451 "app_settings_type", String(255), nullable=True, unique=None,
452 default=None)
452 default=None)
453
453
454 repository = relationship('Repository')
454 repository = relationship('Repository')
455
455
456 def __init__(self, repository_id, key='', val='', type='unicode'):
456 def __init__(self, repository_id, key='', val='', type='unicode'):
457 self.repository_id = repository_id
457 self.repository_id = repository_id
458 self.app_settings_name = key
458 self.app_settings_name = key
459 self.app_settings_type = type
459 self.app_settings_type = type
460 self.app_settings_value = val
460 self.app_settings_value = val
461
461
462 @validates('_app_settings_value')
462 @validates('_app_settings_value')
463 def validate_settings_value(self, key, val):
463 def validate_settings_value(self, key, val):
464 assert type(val) == unicode
464 assert type(val) == unicode
465 return val
465 return val
466
466
467 @hybrid_property
467 @hybrid_property
468 def app_settings_value(self):
468 def app_settings_value(self):
469 v = self._app_settings_value
469 v = self._app_settings_value
470 type_ = self.app_settings_type
470 type_ = self.app_settings_type
471 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
471 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
472 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
472 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
473 return converter(v)
473 return converter(v)
474
474
475 @app_settings_value.setter
475 @app_settings_value.setter
476 def app_settings_value(self, val):
476 def app_settings_value(self, val):
477 """
477 """
478 Setter that will always make sure we use unicode in app_settings_value
478 Setter that will always make sure we use unicode in app_settings_value
479
479
480 :param val:
480 :param val:
481 """
481 """
482 self._app_settings_value = safe_unicode(val)
482 self._app_settings_value = safe_unicode(val)
483
483
484 @hybrid_property
484 @hybrid_property
485 def app_settings_type(self):
485 def app_settings_type(self):
486 return self._app_settings_type
486 return self._app_settings_type
487
487
488 @app_settings_type.setter
488 @app_settings_type.setter
489 def app_settings_type(self, val):
489 def app_settings_type(self, val):
490 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
490 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
491 if val not in SETTINGS_TYPES:
491 if val not in SETTINGS_TYPES:
492 raise Exception('type must be one of %s got %s'
492 raise Exception('type must be one of %s got %s'
493 % (SETTINGS_TYPES.keys(), val))
493 % (SETTINGS_TYPES.keys(), val))
494 self._app_settings_type = val
494 self._app_settings_type = val
495
495
496 def __unicode__(self):
496 def __unicode__(self):
497 return u"<%s('%s:%s:%s[%s]')>" % (
497 return u"<%s('%s:%s:%s[%s]')>" % (
498 self.__class__.__name__, self.repository.repo_name,
498 self.__class__.__name__, self.repository.repo_name,
499 self.app_settings_name, self.app_settings_value,
499 self.app_settings_name, self.app_settings_value,
500 self.app_settings_type
500 self.app_settings_type
501 )
501 )
502
502
503
503
504 class RepoRhodeCodeUi(Base, BaseModel):
504 class RepoRhodeCodeUi(Base, BaseModel):
505 __tablename__ = 'repo_rhodecode_ui'
505 __tablename__ = 'repo_rhodecode_ui'
506 __table_args__ = (
506 __table_args__ = (
507 UniqueConstraint(
507 UniqueConstraint(
508 'repository_id', 'ui_section', 'ui_key',
508 'repository_id', 'ui_section', 'ui_key',
509 name='uq_repo_rhodecode_ui_repository_id_section_key'),
509 name='uq_repo_rhodecode_ui_repository_id_section_key'),
510 base_table_args
510 base_table_args
511 )
511 )
512
512
513 repository_id = Column(
513 repository_id = Column(
514 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
514 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
515 nullable=False)
515 nullable=False)
516 ui_id = Column(
516 ui_id = Column(
517 "ui_id", Integer(), nullable=False, unique=True, default=None,
517 "ui_id", Integer(), nullable=False, unique=True, default=None,
518 primary_key=True)
518 primary_key=True)
519 ui_section = Column(
519 ui_section = Column(
520 "ui_section", String(255), nullable=True, unique=None, default=None)
520 "ui_section", String(255), nullable=True, unique=None, default=None)
521 ui_key = Column(
521 ui_key = Column(
522 "ui_key", String(255), nullable=True, unique=None, default=None)
522 "ui_key", String(255), nullable=True, unique=None, default=None)
523 ui_value = Column(
523 ui_value = Column(
524 "ui_value", String(255), nullable=True, unique=None, default=None)
524 "ui_value", String(255), nullable=True, unique=None, default=None)
525 ui_active = Column(
525 ui_active = Column(
526 "ui_active", Boolean(), nullable=True, unique=None, default=True)
526 "ui_active", Boolean(), nullable=True, unique=None, default=True)
527
527
528 repository = relationship('Repository')
528 repository = relationship('Repository')
529
529
530 def __repr__(self):
530 def __repr__(self):
531 return '<%s[%s:%s]%s=>%s]>' % (
531 return '<%s[%s:%s]%s=>%s]>' % (
532 self.__class__.__name__, self.repository.repo_name,
532 self.__class__.__name__, self.repository.repo_name,
533 self.ui_section, self.ui_key, self.ui_value)
533 self.ui_section, self.ui_key, self.ui_value)
534
534
535
535
536 class User(Base, BaseModel):
536 class User(Base, BaseModel):
537 __tablename__ = 'users'
537 __tablename__ = 'users'
538 __table_args__ = (
538 __table_args__ = (
539 UniqueConstraint('username'), UniqueConstraint('email'),
539 UniqueConstraint('username'), UniqueConstraint('email'),
540 Index('u_username_idx', 'username'),
540 Index('u_username_idx', 'username'),
541 Index('u_email_idx', 'email'),
541 Index('u_email_idx', 'email'),
542 base_table_args
542 base_table_args
543 )
543 )
544
544
545 DEFAULT_USER = 'default'
545 DEFAULT_USER = 'default'
546 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
546 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
547 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
547 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
548
548
549 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
549 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
550 username = Column("username", String(255), nullable=True, unique=None, default=None)
550 username = Column("username", String(255), nullable=True, unique=None, default=None)
551 password = Column("password", String(255), nullable=True, unique=None, default=None)
551 password = Column("password", String(255), nullable=True, unique=None, default=None)
552 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
552 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
553 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
553 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
554 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
554 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
555 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
555 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
556 _email = Column("email", String(255), nullable=True, unique=None, default=None)
556 _email = Column("email", String(255), nullable=True, unique=None, default=None)
557 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
557 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
558 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
558 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
559
559
560 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
560 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
561 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
561 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
562 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
562 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
563 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
563 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
564 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
564 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
565 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
565 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
566
566
567 user_log = relationship('UserLog')
567 user_log = relationship('UserLog')
568 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
568 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
569
569
570 repositories = relationship('Repository')
570 repositories = relationship('Repository')
571 repository_groups = relationship('RepoGroup')
571 repository_groups = relationship('RepoGroup')
572 user_groups = relationship('UserGroup')
572 user_groups = relationship('UserGroup')
573
573
574 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
574 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
575 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
575 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
576
576
577 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
577 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
578 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
578 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
579 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
579 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
580
580
581 group_member = relationship('UserGroupMember', cascade='all')
581 group_member = relationship('UserGroupMember', cascade='all')
582
582
583 notifications = relationship('UserNotification', cascade='all')
583 notifications = relationship('UserNotification', cascade='all')
584 # notifications assigned to this user
584 # notifications assigned to this user
585 user_created_notifications = relationship('Notification', cascade='all')
585 user_created_notifications = relationship('Notification', cascade='all')
586 # comments created by this user
586 # comments created by this user
587 user_comments = relationship('ChangesetComment', cascade='all')
587 user_comments = relationship('ChangesetComment', cascade='all')
588 # user profile extra info
588 # user profile extra info
589 user_emails = relationship('UserEmailMap', cascade='all')
589 user_emails = relationship('UserEmailMap', cascade='all')
590 user_ip_map = relationship('UserIpMap', cascade='all')
590 user_ip_map = relationship('UserIpMap', cascade='all')
591 user_auth_tokens = relationship('UserApiKeys', cascade='all')
591 user_auth_tokens = relationship('UserApiKeys', cascade='all')
592 user_ssh_keys = relationship('UserSshKeys', cascade='all')
592 user_ssh_keys = relationship('UserSshKeys', cascade='all')
593
593
594 # gists
594 # gists
595 user_gists = relationship('Gist', cascade='all')
595 user_gists = relationship('Gist', cascade='all')
596 # user pull requests
596 # user pull requests
597 user_pull_requests = relationship('PullRequest', cascade='all')
597 user_pull_requests = relationship('PullRequest', cascade='all')
598 # external identities
598 # external identities
599 extenal_identities = relationship(
599 extenal_identities = relationship(
600 'ExternalIdentity',
600 'ExternalIdentity',
601 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
601 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
602 cascade='all')
602 cascade='all')
603 # review rules
603 # review rules
604 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
604 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
605
605
606 def __unicode__(self):
606 def __unicode__(self):
607 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
607 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
608 self.user_id, self.username)
608 self.user_id, self.username)
609
609
610 @hybrid_property
610 @hybrid_property
611 def email(self):
611 def email(self):
612 return self._email
612 return self._email
613
613
614 @email.setter
614 @email.setter
615 def email(self, val):
615 def email(self, val):
616 self._email = val.lower() if val else None
616 self._email = val.lower() if val else None
617
617
618 @hybrid_property
618 @hybrid_property
619 def first_name(self):
619 def first_name(self):
620 from rhodecode.lib import helpers as h
620 from rhodecode.lib import helpers as h
621 if self.name:
621 if self.name:
622 return h.escape(self.name)
622 return h.escape(self.name)
623 return self.name
623 return self.name
624
624
625 @hybrid_property
625 @hybrid_property
626 def last_name(self):
626 def last_name(self):
627 from rhodecode.lib import helpers as h
627 from rhodecode.lib import helpers as h
628 if self.lastname:
628 if self.lastname:
629 return h.escape(self.lastname)
629 return h.escape(self.lastname)
630 return self.lastname
630 return self.lastname
631
631
632 @hybrid_property
632 @hybrid_property
633 def api_key(self):
633 def api_key(self):
634 """
634 """
635 Fetch if exist an auth-token with role ALL connected to this user
635 Fetch if exist an auth-token with role ALL connected to this user
636 """
636 """
637 user_auth_token = UserApiKeys.query()\
637 user_auth_token = UserApiKeys.query()\
638 .filter(UserApiKeys.user_id == self.user_id)\
638 .filter(UserApiKeys.user_id == self.user_id)\
639 .filter(or_(UserApiKeys.expires == -1,
639 .filter(or_(UserApiKeys.expires == -1,
640 UserApiKeys.expires >= time.time()))\
640 UserApiKeys.expires >= time.time()))\
641 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
641 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
642 if user_auth_token:
642 if user_auth_token:
643 user_auth_token = user_auth_token.api_key
643 user_auth_token = user_auth_token.api_key
644
644
645 return user_auth_token
645 return user_auth_token
646
646
647 @api_key.setter
647 @api_key.setter
648 def api_key(self, val):
648 def api_key(self, val):
649 # don't allow to set API key this is deprecated for now
649 # don't allow to set API key this is deprecated for now
650 self._api_key = None
650 self._api_key = None
651
651
652 @property
652 @property
653 def reviewer_pull_requests(self):
653 def reviewer_pull_requests(self):
654 return PullRequestReviewers.query() \
654 return PullRequestReviewers.query() \
655 .options(joinedload(PullRequestReviewers.pull_request)) \
655 .options(joinedload(PullRequestReviewers.pull_request)) \
656 .filter(PullRequestReviewers.user_id == self.user_id) \
656 .filter(PullRequestReviewers.user_id == self.user_id) \
657 .all()
657 .all()
658
658
659 @property
659 @property
660 def firstname(self):
660 def firstname(self):
661 # alias for future
661 # alias for future
662 return self.name
662 return self.name
663
663
664 @property
664 @property
665 def emails(self):
665 def emails(self):
666 other = UserEmailMap.query()\
666 other = UserEmailMap.query()\
667 .filter(UserEmailMap.user == self) \
667 .filter(UserEmailMap.user == self) \
668 .order_by(UserEmailMap.email_id.asc()) \
668 .order_by(UserEmailMap.email_id.asc()) \
669 .all()
669 .all()
670 return [self.email] + [x.email for x in other]
670 return [self.email] + [x.email for x in other]
671
671
672 @property
672 @property
673 def auth_tokens(self):
673 def auth_tokens(self):
674 auth_tokens = self.get_auth_tokens()
674 auth_tokens = self.get_auth_tokens()
675 return [x.api_key for x in auth_tokens]
675 return [x.api_key for x in auth_tokens]
676
676
677 def get_auth_tokens(self):
677 def get_auth_tokens(self):
678 return UserApiKeys.query()\
678 return UserApiKeys.query()\
679 .filter(UserApiKeys.user == self)\
679 .filter(UserApiKeys.user == self)\
680 .order_by(UserApiKeys.user_api_key_id.asc())\
680 .order_by(UserApiKeys.user_api_key_id.asc())\
681 .all()
681 .all()
682
682
683 @LazyProperty
683 @LazyProperty
684 def feed_token(self):
684 def feed_token(self):
685 return self.get_feed_token()
685 return self.get_feed_token()
686
686
687 def get_feed_token(self, cache=True):
687 def get_feed_token(self, cache=True):
688 feed_tokens = UserApiKeys.query()\
688 feed_tokens = UserApiKeys.query()\
689 .filter(UserApiKeys.user == self)\
689 .filter(UserApiKeys.user == self)\
690 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
690 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
691 if cache:
691 if cache:
692 feed_tokens = feed_tokens.options(
692 feed_tokens = feed_tokens.options(
693 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
693 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
694
694
695 feed_tokens = feed_tokens.all()
695 feed_tokens = feed_tokens.all()
696 if feed_tokens:
696 if feed_tokens:
697 return feed_tokens[0].api_key
697 return feed_tokens[0].api_key
698 return 'NO_FEED_TOKEN_AVAILABLE'
698 return 'NO_FEED_TOKEN_AVAILABLE'
699
699
700 @classmethod
700 @classmethod
701 def get(cls, user_id, cache=False):
701 def get(cls, user_id, cache=False):
702 if not user_id:
702 if not user_id:
703 return
703 return
704
704
705 user = cls.query()
705 user = cls.query()
706 if cache:
706 if cache:
707 user = user.options(
707 user = user.options(
708 FromCache("sql_cache_short", "get_users_%s" % user_id))
708 FromCache("sql_cache_short", "get_users_%s" % user_id))
709 return user.get(user_id)
709 return user.get(user_id)
710
710
711 @classmethod
711 @classmethod
712 def extra_valid_auth_tokens(cls, user, role=None):
712 def extra_valid_auth_tokens(cls, user, role=None):
713 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
713 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
714 .filter(or_(UserApiKeys.expires == -1,
714 .filter(or_(UserApiKeys.expires == -1,
715 UserApiKeys.expires >= time.time()))
715 UserApiKeys.expires >= time.time()))
716 if role:
716 if role:
717 tokens = tokens.filter(or_(UserApiKeys.role == role,
717 tokens = tokens.filter(or_(UserApiKeys.role == role,
718 UserApiKeys.role == UserApiKeys.ROLE_ALL))
718 UserApiKeys.role == UserApiKeys.ROLE_ALL))
719 return tokens.all()
719 return tokens.all()
720
720
721 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
721 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
722 from rhodecode.lib import auth
722 from rhodecode.lib import auth
723
723
724 log.debug('Trying to authenticate user: %s via auth-token, '
724 log.debug('Trying to authenticate user: %s via auth-token, '
725 'and roles: %s', self, roles)
725 'and roles: %s', self, roles)
726
726
727 if not auth_token:
727 if not auth_token:
728 return False
728 return False
729
729
730 crypto_backend = auth.crypto_backend()
730 crypto_backend = auth.crypto_backend()
731
731
732 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
732 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
733 tokens_q = UserApiKeys.query()\
733 tokens_q = UserApiKeys.query()\
734 .filter(UserApiKeys.user_id == self.user_id)\
734 .filter(UserApiKeys.user_id == self.user_id)\
735 .filter(or_(UserApiKeys.expires == -1,
735 .filter(or_(UserApiKeys.expires == -1,
736 UserApiKeys.expires >= time.time()))
736 UserApiKeys.expires >= time.time()))
737
737
738 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
738 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
739
739
740 plain_tokens = []
740 plain_tokens = []
741 hash_tokens = []
741 hash_tokens = []
742
742
743 for token in tokens_q.all():
743 for token in tokens_q.all():
744 # verify scope first
744 # verify scope first
745 if token.repo_id:
745 if token.repo_id:
746 # token has a scope, we need to verify it
746 # token has a scope, we need to verify it
747 if scope_repo_id != token.repo_id:
747 if scope_repo_id != token.repo_id:
748 log.debug(
748 log.debug(
749 'Scope mismatch: token has a set repo scope: %s, '
749 'Scope mismatch: token has a set repo scope: %s, '
750 'and calling scope is:%s, skipping further checks',
750 'and calling scope is:%s, skipping further checks',
751 token.repo, scope_repo_id)
751 token.repo, scope_repo_id)
752 # token has a scope, and it doesn't match, skip token
752 # token has a scope, and it doesn't match, skip token
753 continue
753 continue
754
754
755 if token.api_key.startswith(crypto_backend.ENC_PREF):
755 if token.api_key.startswith(crypto_backend.ENC_PREF):
756 hash_tokens.append(token.api_key)
756 hash_tokens.append(token.api_key)
757 else:
757 else:
758 plain_tokens.append(token.api_key)
758 plain_tokens.append(token.api_key)
759
759
760 is_plain_match = auth_token in plain_tokens
760 is_plain_match = auth_token in plain_tokens
761 if is_plain_match:
761 if is_plain_match:
762 return True
762 return True
763
763
764 for hashed in hash_tokens:
764 for hashed in hash_tokens:
765 # TODO(marcink): this is expensive to calculate, but most secure
765 # TODO(marcink): this is expensive to calculate, but most secure
766 match = crypto_backend.hash_check(auth_token, hashed)
766 match = crypto_backend.hash_check(auth_token, hashed)
767 if match:
767 if match:
768 return True
768 return True
769
769
770 return False
770 return False
771
771
772 @property
772 @property
773 def ip_addresses(self):
773 def ip_addresses(self):
774 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
774 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
775 return [x.ip_addr for x in ret]
775 return [x.ip_addr for x in ret]
776
776
777 @property
777 @property
778 def username_and_name(self):
778 def username_and_name(self):
779 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
779 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
780
780
781 @property
781 @property
782 def username_or_name_or_email(self):
782 def username_or_name_or_email(self):
783 full_name = self.full_name if self.full_name is not ' ' else None
783 full_name = self.full_name if self.full_name is not ' ' else None
784 return self.username or full_name or self.email
784 return self.username or full_name or self.email
785
785
786 @property
786 @property
787 def full_name(self):
787 def full_name(self):
788 return '%s %s' % (self.first_name, self.last_name)
788 return '%s %s' % (self.first_name, self.last_name)
789
789
790 @property
790 @property
791 def full_name_or_username(self):
791 def full_name_or_username(self):
792 return ('%s %s' % (self.first_name, self.last_name)
792 return ('%s %s' % (self.first_name, self.last_name)
793 if (self.first_name and self.last_name) else self.username)
793 if (self.first_name and self.last_name) else self.username)
794
794
795 @property
795 @property
796 def full_contact(self):
796 def full_contact(self):
797 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
797 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
798
798
799 @property
799 @property
800 def short_contact(self):
800 def short_contact(self):
801 return '%s %s' % (self.first_name, self.last_name)
801 return '%s %s' % (self.first_name, self.last_name)
802
802
803 @property
803 @property
804 def is_admin(self):
804 def is_admin(self):
805 return self.admin
805 return self.admin
806
806
807 def AuthUser(self, **kwargs):
807 def AuthUser(self, **kwargs):
808 """
808 """
809 Returns instance of AuthUser for this user
809 Returns instance of AuthUser for this user
810 """
810 """
811 from rhodecode.lib.auth import AuthUser
811 from rhodecode.lib.auth import AuthUser
812 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
812 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
813
813
814 @hybrid_property
814 @hybrid_property
815 def user_data(self):
815 def user_data(self):
816 if not self._user_data:
816 if not self._user_data:
817 return {}
817 return {}
818
818
819 try:
819 try:
820 return json.loads(self._user_data)
820 return json.loads(self._user_data)
821 except TypeError:
821 except TypeError:
822 return {}
822 return {}
823
823
824 @user_data.setter
824 @user_data.setter
825 def user_data(self, val):
825 def user_data(self, val):
826 if not isinstance(val, dict):
826 if not isinstance(val, dict):
827 raise Exception('user_data must be dict, got %s' % type(val))
827 raise Exception('user_data must be dict, got %s' % type(val))
828 try:
828 try:
829 self._user_data = json.dumps(val)
829 self._user_data = json.dumps(val)
830 except Exception:
830 except Exception:
831 log.error(traceback.format_exc())
831 log.error(traceback.format_exc())
832
832
833 @classmethod
833 @classmethod
834 def get_by_username(cls, username, case_insensitive=False,
834 def get_by_username(cls, username, case_insensitive=False,
835 cache=False, identity_cache=False):
835 cache=False, identity_cache=False):
836 session = Session()
836 session = Session()
837
837
838 if case_insensitive:
838 if case_insensitive:
839 q = cls.query().filter(
839 q = cls.query().filter(
840 func.lower(cls.username) == func.lower(username))
840 func.lower(cls.username) == func.lower(username))
841 else:
841 else:
842 q = cls.query().filter(cls.username == username)
842 q = cls.query().filter(cls.username == username)
843
843
844 if cache:
844 if cache:
845 if identity_cache:
845 if identity_cache:
846 val = cls.identity_cache(session, 'username', username)
846 val = cls.identity_cache(session, 'username', username)
847 if val:
847 if val:
848 return val
848 return val
849 else:
849 else:
850 cache_key = "get_user_by_name_%s" % _hash_key(username)
850 cache_key = "get_user_by_name_%s" % _hash_key(username)
851 q = q.options(
851 q = q.options(
852 FromCache("sql_cache_short", cache_key))
852 FromCache("sql_cache_short", cache_key))
853
853
854 return q.scalar()
854 return q.scalar()
855
855
856 @classmethod
856 @classmethod
857 def get_by_auth_token(cls, auth_token, cache=False):
857 def get_by_auth_token(cls, auth_token, cache=False):
858 q = UserApiKeys.query()\
858 q = UserApiKeys.query()\
859 .filter(UserApiKeys.api_key == auth_token)\
859 .filter(UserApiKeys.api_key == auth_token)\
860 .filter(or_(UserApiKeys.expires == -1,
860 .filter(or_(UserApiKeys.expires == -1,
861 UserApiKeys.expires >= time.time()))
861 UserApiKeys.expires >= time.time()))
862 if cache:
862 if cache:
863 q = q.options(
863 q = q.options(
864 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
864 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
865
865
866 match = q.first()
866 match = q.first()
867 if match:
867 if match:
868 return match.user
868 return match.user
869
869
870 @classmethod
870 @classmethod
871 def get_by_email(cls, email, case_insensitive=False, cache=False):
871 def get_by_email(cls, email, case_insensitive=False, cache=False):
872
872
873 if case_insensitive:
873 if case_insensitive:
874 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
874 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
875
875
876 else:
876 else:
877 q = cls.query().filter(cls.email == email)
877 q = cls.query().filter(cls.email == email)
878
878
879 email_key = _hash_key(email)
879 email_key = _hash_key(email)
880 if cache:
880 if cache:
881 q = q.options(
881 q = q.options(
882 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
882 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
883
883
884 ret = q.scalar()
884 ret = q.scalar()
885 if ret is None:
885 if ret is None:
886 q = UserEmailMap.query()
886 q = UserEmailMap.query()
887 # try fetching in alternate email map
887 # try fetching in alternate email map
888 if case_insensitive:
888 if case_insensitive:
889 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
889 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
890 else:
890 else:
891 q = q.filter(UserEmailMap.email == email)
891 q = q.filter(UserEmailMap.email == email)
892 q = q.options(joinedload(UserEmailMap.user))
892 q = q.options(joinedload(UserEmailMap.user))
893 if cache:
893 if cache:
894 q = q.options(
894 q = q.options(
895 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
895 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
896 ret = getattr(q.scalar(), 'user', None)
896 ret = getattr(q.scalar(), 'user', None)
897
897
898 return ret
898 return ret
899
899
900 @classmethod
900 @classmethod
901 def get_from_cs_author(cls, author):
901 def get_from_cs_author(cls, author):
902 """
902 """
903 Tries to get User objects out of commit author string
903 Tries to get User objects out of commit author string
904
904
905 :param author:
905 :param author:
906 """
906 """
907 from rhodecode.lib.helpers import email, author_name
907 from rhodecode.lib.helpers import email, author_name
908 # Valid email in the attribute passed, see if they're in the system
908 # Valid email in the attribute passed, see if they're in the system
909 _email = email(author)
909 _email = email(author)
910 if _email:
910 if _email:
911 user = cls.get_by_email(_email, case_insensitive=True)
911 user = cls.get_by_email(_email, case_insensitive=True)
912 if user:
912 if user:
913 return user
913 return user
914 # Maybe we can match by username?
914 # Maybe we can match by username?
915 _author = author_name(author)
915 _author = author_name(author)
916 user = cls.get_by_username(_author, case_insensitive=True)
916 user = cls.get_by_username(_author, case_insensitive=True)
917 if user:
917 if user:
918 return user
918 return user
919
919
920 def update_userdata(self, **kwargs):
920 def update_userdata(self, **kwargs):
921 usr = self
921 usr = self
922 old = usr.user_data
922 old = usr.user_data
923 old.update(**kwargs)
923 old.update(**kwargs)
924 usr.user_data = old
924 usr.user_data = old
925 Session().add(usr)
925 Session().add(usr)
926 log.debug('updated userdata with ', kwargs)
926 log.debug('updated userdata with ', kwargs)
927
927
928 def update_lastlogin(self):
928 def update_lastlogin(self):
929 """Update user lastlogin"""
929 """Update user lastlogin"""
930 self.last_login = datetime.datetime.now()
930 self.last_login = datetime.datetime.now()
931 Session().add(self)
931 Session().add(self)
932 log.debug('updated user %s lastlogin', self.username)
932 log.debug('updated user %s lastlogin', self.username)
933
933
934 def update_lastactivity(self):
935 """Update user lastactivity"""
936 self.last_activity = datetime.datetime.now()
937 Session().add(self)
938 log.debug('updated user `%s` last activity', self.username)
939
940 def update_password(self, new_password):
934 def update_password(self, new_password):
941 from rhodecode.lib.auth import get_crypt_password
935 from rhodecode.lib.auth import get_crypt_password
942
936
943 self.password = get_crypt_password(new_password)
937 self.password = get_crypt_password(new_password)
944 Session().add(self)
938 Session().add(self)
945
939
946 @classmethod
940 @classmethod
947 def get_first_super_admin(cls):
941 def get_first_super_admin(cls):
948 user = User.query().filter(User.admin == true()).first()
942 user = User.query().filter(User.admin == true()).first()
949 if user is None:
943 if user is None:
950 raise Exception('FATAL: Missing administrative account!')
944 raise Exception('FATAL: Missing administrative account!')
951 return user
945 return user
952
946
953 @classmethod
947 @classmethod
954 def get_all_super_admins(cls):
948 def get_all_super_admins(cls):
955 """
949 """
956 Returns all admin accounts sorted by username
950 Returns all admin accounts sorted by username
957 """
951 """
958 return User.query().filter(User.admin == true())\
952 return User.query().filter(User.admin == true())\
959 .order_by(User.username.asc()).all()
953 .order_by(User.username.asc()).all()
960
954
961 @classmethod
955 @classmethod
962 def get_default_user(cls, cache=False, refresh=False):
956 def get_default_user(cls, cache=False, refresh=False):
963 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
957 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
964 if user is None:
958 if user is None:
965 raise Exception('FATAL: Missing default account!')
959 raise Exception('FATAL: Missing default account!')
966 if refresh:
960 if refresh:
967 # The default user might be based on outdated state which
961 # The default user might be based on outdated state which
968 # has been loaded from the cache.
962 # has been loaded from the cache.
969 # A call to refresh() ensures that the
963 # A call to refresh() ensures that the
970 # latest state from the database is used.
964 # latest state from the database is used.
971 Session().refresh(user)
965 Session().refresh(user)
972 return user
966 return user
973
967
974 def _get_default_perms(self, user, suffix=''):
968 def _get_default_perms(self, user, suffix=''):
975 from rhodecode.model.permission import PermissionModel
969 from rhodecode.model.permission import PermissionModel
976 return PermissionModel().get_default_perms(user.user_perms, suffix)
970 return PermissionModel().get_default_perms(user.user_perms, suffix)
977
971
978 def get_default_perms(self, suffix=''):
972 def get_default_perms(self, suffix=''):
979 return self._get_default_perms(self, suffix)
973 return self._get_default_perms(self, suffix)
980
974
981 def get_api_data(self, include_secrets=False, details='full'):
975 def get_api_data(self, include_secrets=False, details='full'):
982 """
976 """
983 Common function for generating user related data for API
977 Common function for generating user related data for API
984
978
985 :param include_secrets: By default secrets in the API data will be replaced
979 :param include_secrets: By default secrets in the API data will be replaced
986 by a placeholder value to prevent exposing this data by accident. In case
980 by a placeholder value to prevent exposing this data by accident. In case
987 this data shall be exposed, set this flag to ``True``.
981 this data shall be exposed, set this flag to ``True``.
988
982
989 :param details: details can be 'basic|full' basic gives only a subset of
983 :param details: details can be 'basic|full' basic gives only a subset of
990 the available user information that includes user_id, name and emails.
984 the available user information that includes user_id, name and emails.
991 """
985 """
992 user = self
986 user = self
993 user_data = self.user_data
987 user_data = self.user_data
994 data = {
988 data = {
995 'user_id': user.user_id,
989 'user_id': user.user_id,
996 'username': user.username,
990 'username': user.username,
997 'firstname': user.name,
991 'firstname': user.name,
998 'lastname': user.lastname,
992 'lastname': user.lastname,
999 'email': user.email,
993 'email': user.email,
1000 'emails': user.emails,
994 'emails': user.emails,
1001 }
995 }
1002 if details == 'basic':
996 if details == 'basic':
1003 return data
997 return data
1004
998
1005 auth_token_length = 40
999 auth_token_length = 40
1006 auth_token_replacement = '*' * auth_token_length
1000 auth_token_replacement = '*' * auth_token_length
1007
1001
1008 extras = {
1002 extras = {
1009 'auth_tokens': [auth_token_replacement],
1003 'auth_tokens': [auth_token_replacement],
1010 'active': user.active,
1004 'active': user.active,
1011 'admin': user.admin,
1005 'admin': user.admin,
1012 'extern_type': user.extern_type,
1006 'extern_type': user.extern_type,
1013 'extern_name': user.extern_name,
1007 'extern_name': user.extern_name,
1014 'last_login': user.last_login,
1008 'last_login': user.last_login,
1015 'last_activity': user.last_activity,
1009 'last_activity': user.last_activity,
1016 'ip_addresses': user.ip_addresses,
1010 'ip_addresses': user.ip_addresses,
1017 'language': user_data.get('language')
1011 'language': user_data.get('language')
1018 }
1012 }
1019 data.update(extras)
1013 data.update(extras)
1020
1014
1021 if include_secrets:
1015 if include_secrets:
1022 data['auth_tokens'] = user.auth_tokens
1016 data['auth_tokens'] = user.auth_tokens
1023 return data
1017 return data
1024
1018
1025 def __json__(self):
1019 def __json__(self):
1026 data = {
1020 data = {
1027 'full_name': self.full_name,
1021 'full_name': self.full_name,
1028 'full_name_or_username': self.full_name_or_username,
1022 'full_name_or_username': self.full_name_or_username,
1029 'short_contact': self.short_contact,
1023 'short_contact': self.short_contact,
1030 'full_contact': self.full_contact,
1024 'full_contact': self.full_contact,
1031 }
1025 }
1032 data.update(self.get_api_data())
1026 data.update(self.get_api_data())
1033 return data
1027 return data
1034
1028
1035
1029
1036 class UserApiKeys(Base, BaseModel):
1030 class UserApiKeys(Base, BaseModel):
1037 __tablename__ = 'user_api_keys'
1031 __tablename__ = 'user_api_keys'
1038 __table_args__ = (
1032 __table_args__ = (
1039 Index('uak_api_key_idx', 'api_key', unique=True),
1033 Index('uak_api_key_idx', 'api_key', unique=True),
1040 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1034 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1041 base_table_args
1035 base_table_args
1042 )
1036 )
1043 __mapper_args__ = {}
1037 __mapper_args__ = {}
1044
1038
1045 # ApiKey role
1039 # ApiKey role
1046 ROLE_ALL = 'token_role_all'
1040 ROLE_ALL = 'token_role_all'
1047 ROLE_HTTP = 'token_role_http'
1041 ROLE_HTTP = 'token_role_http'
1048 ROLE_VCS = 'token_role_vcs'
1042 ROLE_VCS = 'token_role_vcs'
1049 ROLE_API = 'token_role_api'
1043 ROLE_API = 'token_role_api'
1050 ROLE_FEED = 'token_role_feed'
1044 ROLE_FEED = 'token_role_feed'
1051 ROLE_PASSWORD_RESET = 'token_password_reset'
1045 ROLE_PASSWORD_RESET = 'token_password_reset'
1052
1046
1053 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1047 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1054
1048
1055 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1056 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1057 api_key = Column("api_key", String(255), nullable=False, unique=True)
1051 api_key = Column("api_key", String(255), nullable=False, unique=True)
1058 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1052 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1059 expires = Column('expires', Float(53), nullable=False)
1053 expires = Column('expires', Float(53), nullable=False)
1060 role = Column('role', String(255), nullable=True)
1054 role = Column('role', String(255), nullable=True)
1061 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1055 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1062
1056
1063 # scope columns
1057 # scope columns
1064 repo_id = Column(
1058 repo_id = Column(
1065 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1059 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1066 nullable=True, unique=None, default=None)
1060 nullable=True, unique=None, default=None)
1067 repo = relationship('Repository', lazy='joined')
1061 repo = relationship('Repository', lazy='joined')
1068
1062
1069 repo_group_id = Column(
1063 repo_group_id = Column(
1070 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1064 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1071 nullable=True, unique=None, default=None)
1065 nullable=True, unique=None, default=None)
1072 repo_group = relationship('RepoGroup', lazy='joined')
1066 repo_group = relationship('RepoGroup', lazy='joined')
1073
1067
1074 user = relationship('User', lazy='joined')
1068 user = relationship('User', lazy='joined')
1075
1069
1076 def __unicode__(self):
1070 def __unicode__(self):
1077 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1071 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1078
1072
1079 def __json__(self):
1073 def __json__(self):
1080 data = {
1074 data = {
1081 'auth_token': self.api_key,
1075 'auth_token': self.api_key,
1082 'role': self.role,
1076 'role': self.role,
1083 'scope': self.scope_humanized,
1077 'scope': self.scope_humanized,
1084 'expired': self.expired
1078 'expired': self.expired
1085 }
1079 }
1086 return data
1080 return data
1087
1081
1088 def get_api_data(self, include_secrets=False):
1082 def get_api_data(self, include_secrets=False):
1089 data = self.__json__()
1083 data = self.__json__()
1090 if include_secrets:
1084 if include_secrets:
1091 return data
1085 return data
1092 else:
1086 else:
1093 data['auth_token'] = self.token_obfuscated
1087 data['auth_token'] = self.token_obfuscated
1094 return data
1088 return data
1095
1089
1096 @hybrid_property
1090 @hybrid_property
1097 def description_safe(self):
1091 def description_safe(self):
1098 from rhodecode.lib import helpers as h
1092 from rhodecode.lib import helpers as h
1099 return h.escape(self.description)
1093 return h.escape(self.description)
1100
1094
1101 @property
1095 @property
1102 def expired(self):
1096 def expired(self):
1103 if self.expires == -1:
1097 if self.expires == -1:
1104 return False
1098 return False
1105 return time.time() > self.expires
1099 return time.time() > self.expires
1106
1100
1107 @classmethod
1101 @classmethod
1108 def _get_role_name(cls, role):
1102 def _get_role_name(cls, role):
1109 return {
1103 return {
1110 cls.ROLE_ALL: _('all'),
1104 cls.ROLE_ALL: _('all'),
1111 cls.ROLE_HTTP: _('http/web interface'),
1105 cls.ROLE_HTTP: _('http/web interface'),
1112 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1106 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1113 cls.ROLE_API: _('api calls'),
1107 cls.ROLE_API: _('api calls'),
1114 cls.ROLE_FEED: _('feed access'),
1108 cls.ROLE_FEED: _('feed access'),
1115 }.get(role, role)
1109 }.get(role, role)
1116
1110
1117 @property
1111 @property
1118 def role_humanized(self):
1112 def role_humanized(self):
1119 return self._get_role_name(self.role)
1113 return self._get_role_name(self.role)
1120
1114
1121 def _get_scope(self):
1115 def _get_scope(self):
1122 if self.repo:
1116 if self.repo:
1123 return repr(self.repo)
1117 return repr(self.repo)
1124 if self.repo_group:
1118 if self.repo_group:
1125 return repr(self.repo_group) + ' (recursive)'
1119 return repr(self.repo_group) + ' (recursive)'
1126 return 'global'
1120 return 'global'
1127
1121
1128 @property
1122 @property
1129 def scope_humanized(self):
1123 def scope_humanized(self):
1130 return self._get_scope()
1124 return self._get_scope()
1131
1125
1132 @property
1126 @property
1133 def token_obfuscated(self):
1127 def token_obfuscated(self):
1134 if self.api_key:
1128 if self.api_key:
1135 return self.api_key[:4] + "****"
1129 return self.api_key[:4] + "****"
1136
1130
1137
1131
1138 class UserEmailMap(Base, BaseModel):
1132 class UserEmailMap(Base, BaseModel):
1139 __tablename__ = 'user_email_map'
1133 __tablename__ = 'user_email_map'
1140 __table_args__ = (
1134 __table_args__ = (
1141 Index('uem_email_idx', 'email'),
1135 Index('uem_email_idx', 'email'),
1142 UniqueConstraint('email'),
1136 UniqueConstraint('email'),
1143 base_table_args
1137 base_table_args
1144 )
1138 )
1145 __mapper_args__ = {}
1139 __mapper_args__ = {}
1146
1140
1147 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1141 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1142 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1149 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1143 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1150 user = relationship('User', lazy='joined')
1144 user = relationship('User', lazy='joined')
1151
1145
1152 @validates('_email')
1146 @validates('_email')
1153 def validate_email(self, key, email):
1147 def validate_email(self, key, email):
1154 # check if this email is not main one
1148 # check if this email is not main one
1155 main_email = Session().query(User).filter(User.email == email).scalar()
1149 main_email = Session().query(User).filter(User.email == email).scalar()
1156 if main_email is not None:
1150 if main_email is not None:
1157 raise AttributeError('email %s is present is user table' % email)
1151 raise AttributeError('email %s is present is user table' % email)
1158 return email
1152 return email
1159
1153
1160 @hybrid_property
1154 @hybrid_property
1161 def email(self):
1155 def email(self):
1162 return self._email
1156 return self._email
1163
1157
1164 @email.setter
1158 @email.setter
1165 def email(self, val):
1159 def email(self, val):
1166 self._email = val.lower() if val else None
1160 self._email = val.lower() if val else None
1167
1161
1168
1162
1169 class UserIpMap(Base, BaseModel):
1163 class UserIpMap(Base, BaseModel):
1170 __tablename__ = 'user_ip_map'
1164 __tablename__ = 'user_ip_map'
1171 __table_args__ = (
1165 __table_args__ = (
1172 UniqueConstraint('user_id', 'ip_addr'),
1166 UniqueConstraint('user_id', 'ip_addr'),
1173 base_table_args
1167 base_table_args
1174 )
1168 )
1175 __mapper_args__ = {}
1169 __mapper_args__ = {}
1176
1170
1177 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1171 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1178 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1172 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1179 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1173 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1180 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1174 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1181 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1175 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1182 user = relationship('User', lazy='joined')
1176 user = relationship('User', lazy='joined')
1183
1177
1184 @hybrid_property
1178 @hybrid_property
1185 def description_safe(self):
1179 def description_safe(self):
1186 from rhodecode.lib import helpers as h
1180 from rhodecode.lib import helpers as h
1187 return h.escape(self.description)
1181 return h.escape(self.description)
1188
1182
1189 @classmethod
1183 @classmethod
1190 def _get_ip_range(cls, ip_addr):
1184 def _get_ip_range(cls, ip_addr):
1191 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1185 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1192 return [str(net.network_address), str(net.broadcast_address)]
1186 return [str(net.network_address), str(net.broadcast_address)]
1193
1187
1194 def __json__(self):
1188 def __json__(self):
1195 return {
1189 return {
1196 'ip_addr': self.ip_addr,
1190 'ip_addr': self.ip_addr,
1197 'ip_range': self._get_ip_range(self.ip_addr),
1191 'ip_range': self._get_ip_range(self.ip_addr),
1198 }
1192 }
1199
1193
1200 def __unicode__(self):
1194 def __unicode__(self):
1201 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1195 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1202 self.user_id, self.ip_addr)
1196 self.user_id, self.ip_addr)
1203
1197
1204
1198
1205 class UserSshKeys(Base, BaseModel):
1199 class UserSshKeys(Base, BaseModel):
1206 __tablename__ = 'user_ssh_keys'
1200 __tablename__ = 'user_ssh_keys'
1207 __table_args__ = (
1201 __table_args__ = (
1208 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1202 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1209
1203
1210 UniqueConstraint('ssh_key_fingerprint'),
1204 UniqueConstraint('ssh_key_fingerprint'),
1211
1205
1212 base_table_args
1206 base_table_args
1213 )
1207 )
1214 __mapper_args__ = {}
1208 __mapper_args__ = {}
1215
1209
1216 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1217 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1211 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1218 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1212 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1219
1213
1220 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1214 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1221
1215
1222 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1223 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1217 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1224 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1218 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1225
1219
1226 user = relationship('User', lazy='joined')
1220 user = relationship('User', lazy='joined')
1227
1221
1228 def __json__(self):
1222 def __json__(self):
1229 data = {
1223 data = {
1230 'ssh_fingerprint': self.ssh_key_fingerprint,
1224 'ssh_fingerprint': self.ssh_key_fingerprint,
1231 'description': self.description,
1225 'description': self.description,
1232 'created_on': self.created_on
1226 'created_on': self.created_on
1233 }
1227 }
1234 return data
1228 return data
1235
1229
1236 def get_api_data(self):
1230 def get_api_data(self):
1237 data = self.__json__()
1231 data = self.__json__()
1238 return data
1232 return data
1239
1233
1240
1234
1241 class UserLog(Base, BaseModel):
1235 class UserLog(Base, BaseModel):
1242 __tablename__ = 'user_logs'
1236 __tablename__ = 'user_logs'
1243 __table_args__ = (
1237 __table_args__ = (
1244 base_table_args,
1238 base_table_args,
1245 )
1239 )
1246
1240
1247 VERSION_1 = 'v1'
1241 VERSION_1 = 'v1'
1248 VERSION_2 = 'v2'
1242 VERSION_2 = 'v2'
1249 VERSIONS = [VERSION_1, VERSION_2]
1243 VERSIONS = [VERSION_1, VERSION_2]
1250
1244
1251 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1245 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1252 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1246 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1253 username = Column("username", String(255), nullable=True, unique=None, default=None)
1247 username = Column("username", String(255), nullable=True, unique=None, default=None)
1254 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1248 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1255 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1249 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1256 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1250 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1257 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1251 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1258 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1252 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1259
1253
1260 version = Column("version", String(255), nullable=True, default=VERSION_1)
1254 version = Column("version", String(255), nullable=True, default=VERSION_1)
1261 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1255 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1262 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1256 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1263
1257
1264 def __unicode__(self):
1258 def __unicode__(self):
1265 return u"<%s('id:%s:%s')>" % (
1259 return u"<%s('id:%s:%s')>" % (
1266 self.__class__.__name__, self.repository_name, self.action)
1260 self.__class__.__name__, self.repository_name, self.action)
1267
1261
1268 def __json__(self):
1262 def __json__(self):
1269 return {
1263 return {
1270 'user_id': self.user_id,
1264 'user_id': self.user_id,
1271 'username': self.username,
1265 'username': self.username,
1272 'repository_id': self.repository_id,
1266 'repository_id': self.repository_id,
1273 'repository_name': self.repository_name,
1267 'repository_name': self.repository_name,
1274 'user_ip': self.user_ip,
1268 'user_ip': self.user_ip,
1275 'action_date': self.action_date,
1269 'action_date': self.action_date,
1276 'action': self.action,
1270 'action': self.action,
1277 }
1271 }
1278
1272
1279 @hybrid_property
1273 @hybrid_property
1280 def entry_id(self):
1274 def entry_id(self):
1281 return self.user_log_id
1275 return self.user_log_id
1282
1276
1283 @property
1277 @property
1284 def action_as_day(self):
1278 def action_as_day(self):
1285 return datetime.date(*self.action_date.timetuple()[:3])
1279 return datetime.date(*self.action_date.timetuple()[:3])
1286
1280
1287 user = relationship('User')
1281 user = relationship('User')
1288 repository = relationship('Repository', cascade='')
1282 repository = relationship('Repository', cascade='')
1289
1283
1290
1284
1291 class UserGroup(Base, BaseModel):
1285 class UserGroup(Base, BaseModel):
1292 __tablename__ = 'users_groups'
1286 __tablename__ = 'users_groups'
1293 __table_args__ = (
1287 __table_args__ = (
1294 base_table_args,
1288 base_table_args,
1295 )
1289 )
1296
1290
1297 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1291 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1298 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1292 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1299 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1293 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1300 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1294 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1301 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1295 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1302 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1296 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1303 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1297 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1304 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1298 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1305
1299
1306 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1300 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1307 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1301 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1308 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1302 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1309 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1303 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1310 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1304 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1311 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1305 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1312
1306
1313 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1307 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1314 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1308 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1315
1309
1316 @classmethod
1310 @classmethod
1317 def _load_group_data(cls, column):
1311 def _load_group_data(cls, column):
1318 if not column:
1312 if not column:
1319 return {}
1313 return {}
1320
1314
1321 try:
1315 try:
1322 return json.loads(column) or {}
1316 return json.loads(column) or {}
1323 except TypeError:
1317 except TypeError:
1324 return {}
1318 return {}
1325
1319
1326 @hybrid_property
1320 @hybrid_property
1327 def description_safe(self):
1321 def description_safe(self):
1328 from rhodecode.lib import helpers as h
1322 from rhodecode.lib import helpers as h
1329 return h.escape(self.user_group_description)
1323 return h.escape(self.user_group_description)
1330
1324
1331 @hybrid_property
1325 @hybrid_property
1332 def group_data(self):
1326 def group_data(self):
1333 return self._load_group_data(self._group_data)
1327 return self._load_group_data(self._group_data)
1334
1328
1335 @group_data.expression
1329 @group_data.expression
1336 def group_data(self, **kwargs):
1330 def group_data(self, **kwargs):
1337 return self._group_data
1331 return self._group_data
1338
1332
1339 @group_data.setter
1333 @group_data.setter
1340 def group_data(self, val):
1334 def group_data(self, val):
1341 try:
1335 try:
1342 self._group_data = json.dumps(val)
1336 self._group_data = json.dumps(val)
1343 except Exception:
1337 except Exception:
1344 log.error(traceback.format_exc())
1338 log.error(traceback.format_exc())
1345
1339
1346 @classmethod
1340 @classmethod
1347 def _load_sync(cls, group_data):
1341 def _load_sync(cls, group_data):
1348 if group_data:
1342 if group_data:
1349 return group_data.get('extern_type')
1343 return group_data.get('extern_type')
1350
1344
1351 @property
1345 @property
1352 def sync(self):
1346 def sync(self):
1353 return self._load_sync(self.group_data)
1347 return self._load_sync(self.group_data)
1354
1348
1355 def __unicode__(self):
1349 def __unicode__(self):
1356 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1350 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1357 self.users_group_id,
1351 self.users_group_id,
1358 self.users_group_name)
1352 self.users_group_name)
1359
1353
1360 @classmethod
1354 @classmethod
1361 def get_by_group_name(cls, group_name, cache=False,
1355 def get_by_group_name(cls, group_name, cache=False,
1362 case_insensitive=False):
1356 case_insensitive=False):
1363 if case_insensitive:
1357 if case_insensitive:
1364 q = cls.query().filter(func.lower(cls.users_group_name) ==
1358 q = cls.query().filter(func.lower(cls.users_group_name) ==
1365 func.lower(group_name))
1359 func.lower(group_name))
1366
1360
1367 else:
1361 else:
1368 q = cls.query().filter(cls.users_group_name == group_name)
1362 q = cls.query().filter(cls.users_group_name == group_name)
1369 if cache:
1363 if cache:
1370 q = q.options(
1364 q = q.options(
1371 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1365 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1372 return q.scalar()
1366 return q.scalar()
1373
1367
1374 @classmethod
1368 @classmethod
1375 def get(cls, user_group_id, cache=False):
1369 def get(cls, user_group_id, cache=False):
1376 if not user_group_id:
1370 if not user_group_id:
1377 return
1371 return
1378
1372
1379 user_group = cls.query()
1373 user_group = cls.query()
1380 if cache:
1374 if cache:
1381 user_group = user_group.options(
1375 user_group = user_group.options(
1382 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1376 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1383 return user_group.get(user_group_id)
1377 return user_group.get(user_group_id)
1384
1378
1385 def permissions(self, with_admins=True, with_owner=True):
1379 def permissions(self, with_admins=True, with_owner=True):
1386 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1380 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1387 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1381 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1388 joinedload(UserUserGroupToPerm.user),
1382 joinedload(UserUserGroupToPerm.user),
1389 joinedload(UserUserGroupToPerm.permission),)
1383 joinedload(UserUserGroupToPerm.permission),)
1390
1384
1391 # get owners and admins and permissions. We do a trick of re-writing
1385 # get owners and admins and permissions. We do a trick of re-writing
1392 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1386 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1393 # has a global reference and changing one object propagates to all
1387 # has a global reference and changing one object propagates to all
1394 # others. This means if admin is also an owner admin_row that change
1388 # others. This means if admin is also an owner admin_row that change
1395 # would propagate to both objects
1389 # would propagate to both objects
1396 perm_rows = []
1390 perm_rows = []
1397 for _usr in q.all():
1391 for _usr in q.all():
1398 usr = AttributeDict(_usr.user.get_dict())
1392 usr = AttributeDict(_usr.user.get_dict())
1399 usr.permission = _usr.permission.permission_name
1393 usr.permission = _usr.permission.permission_name
1400 perm_rows.append(usr)
1394 perm_rows.append(usr)
1401
1395
1402 # filter the perm rows by 'default' first and then sort them by
1396 # filter the perm rows by 'default' first and then sort them by
1403 # admin,write,read,none permissions sorted again alphabetically in
1397 # admin,write,read,none permissions sorted again alphabetically in
1404 # each group
1398 # each group
1405 perm_rows = sorted(perm_rows, key=display_user_sort)
1399 perm_rows = sorted(perm_rows, key=display_user_sort)
1406
1400
1407 _admin_perm = 'usergroup.admin'
1401 _admin_perm = 'usergroup.admin'
1408 owner_row = []
1402 owner_row = []
1409 if with_owner:
1403 if with_owner:
1410 usr = AttributeDict(self.user.get_dict())
1404 usr = AttributeDict(self.user.get_dict())
1411 usr.owner_row = True
1405 usr.owner_row = True
1412 usr.permission = _admin_perm
1406 usr.permission = _admin_perm
1413 owner_row.append(usr)
1407 owner_row.append(usr)
1414
1408
1415 super_admin_rows = []
1409 super_admin_rows = []
1416 if with_admins:
1410 if with_admins:
1417 for usr in User.get_all_super_admins():
1411 for usr in User.get_all_super_admins():
1418 # if this admin is also owner, don't double the record
1412 # if this admin is also owner, don't double the record
1419 if usr.user_id == owner_row[0].user_id:
1413 if usr.user_id == owner_row[0].user_id:
1420 owner_row[0].admin_row = True
1414 owner_row[0].admin_row = True
1421 else:
1415 else:
1422 usr = AttributeDict(usr.get_dict())
1416 usr = AttributeDict(usr.get_dict())
1423 usr.admin_row = True
1417 usr.admin_row = True
1424 usr.permission = _admin_perm
1418 usr.permission = _admin_perm
1425 super_admin_rows.append(usr)
1419 super_admin_rows.append(usr)
1426
1420
1427 return super_admin_rows + owner_row + perm_rows
1421 return super_admin_rows + owner_row + perm_rows
1428
1422
1429 def permission_user_groups(self):
1423 def permission_user_groups(self):
1430 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1424 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1431 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1425 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1432 joinedload(UserGroupUserGroupToPerm.target_user_group),
1426 joinedload(UserGroupUserGroupToPerm.target_user_group),
1433 joinedload(UserGroupUserGroupToPerm.permission),)
1427 joinedload(UserGroupUserGroupToPerm.permission),)
1434
1428
1435 perm_rows = []
1429 perm_rows = []
1436 for _user_group in q.all():
1430 for _user_group in q.all():
1437 usr = AttributeDict(_user_group.user_group.get_dict())
1431 usr = AttributeDict(_user_group.user_group.get_dict())
1438 usr.permission = _user_group.permission.permission_name
1432 usr.permission = _user_group.permission.permission_name
1439 perm_rows.append(usr)
1433 perm_rows.append(usr)
1440
1434
1441 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1435 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1442 return perm_rows
1436 return perm_rows
1443
1437
1444 def _get_default_perms(self, user_group, suffix=''):
1438 def _get_default_perms(self, user_group, suffix=''):
1445 from rhodecode.model.permission import PermissionModel
1439 from rhodecode.model.permission import PermissionModel
1446 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1440 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1447
1441
1448 def get_default_perms(self, suffix=''):
1442 def get_default_perms(self, suffix=''):
1449 return self._get_default_perms(self, suffix)
1443 return self._get_default_perms(self, suffix)
1450
1444
1451 def get_api_data(self, with_group_members=True, include_secrets=False):
1445 def get_api_data(self, with_group_members=True, include_secrets=False):
1452 """
1446 """
1453 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1447 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1454 basically forwarded.
1448 basically forwarded.
1455
1449
1456 """
1450 """
1457 user_group = self
1451 user_group = self
1458 data = {
1452 data = {
1459 'users_group_id': user_group.users_group_id,
1453 'users_group_id': user_group.users_group_id,
1460 'group_name': user_group.users_group_name,
1454 'group_name': user_group.users_group_name,
1461 'group_description': user_group.user_group_description,
1455 'group_description': user_group.user_group_description,
1462 'active': user_group.users_group_active,
1456 'active': user_group.users_group_active,
1463 'owner': user_group.user.username,
1457 'owner': user_group.user.username,
1464 'sync': user_group.sync,
1458 'sync': user_group.sync,
1465 'owner_email': user_group.user.email,
1459 'owner_email': user_group.user.email,
1466 }
1460 }
1467
1461
1468 if with_group_members:
1462 if with_group_members:
1469 users = []
1463 users = []
1470 for user in user_group.members:
1464 for user in user_group.members:
1471 user = user.user
1465 user = user.user
1472 users.append(user.get_api_data(include_secrets=include_secrets))
1466 users.append(user.get_api_data(include_secrets=include_secrets))
1473 data['users'] = users
1467 data['users'] = users
1474
1468
1475 return data
1469 return data
1476
1470
1477
1471
1478 class UserGroupMember(Base, BaseModel):
1472 class UserGroupMember(Base, BaseModel):
1479 __tablename__ = 'users_groups_members'
1473 __tablename__ = 'users_groups_members'
1480 __table_args__ = (
1474 __table_args__ = (
1481 base_table_args,
1475 base_table_args,
1482 )
1476 )
1483
1477
1484 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1478 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1485 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1479 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1480 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1487
1481
1488 user = relationship('User', lazy='joined')
1482 user = relationship('User', lazy='joined')
1489 users_group = relationship('UserGroup')
1483 users_group = relationship('UserGroup')
1490
1484
1491 def __init__(self, gr_id='', u_id=''):
1485 def __init__(self, gr_id='', u_id=''):
1492 self.users_group_id = gr_id
1486 self.users_group_id = gr_id
1493 self.user_id = u_id
1487 self.user_id = u_id
1494
1488
1495
1489
1496 class RepositoryField(Base, BaseModel):
1490 class RepositoryField(Base, BaseModel):
1497 __tablename__ = 'repositories_fields'
1491 __tablename__ = 'repositories_fields'
1498 __table_args__ = (
1492 __table_args__ = (
1499 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1493 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1500 base_table_args,
1494 base_table_args,
1501 )
1495 )
1502
1496
1503 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1497 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1504
1498
1505 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1499 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1506 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1500 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1507 field_key = Column("field_key", String(250))
1501 field_key = Column("field_key", String(250))
1508 field_label = Column("field_label", String(1024), nullable=False)
1502 field_label = Column("field_label", String(1024), nullable=False)
1509 field_value = Column("field_value", String(10000), nullable=False)
1503 field_value = Column("field_value", String(10000), nullable=False)
1510 field_desc = Column("field_desc", String(1024), nullable=False)
1504 field_desc = Column("field_desc", String(1024), nullable=False)
1511 field_type = Column("field_type", String(255), nullable=False, unique=None)
1505 field_type = Column("field_type", String(255), nullable=False, unique=None)
1512 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1506 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1513
1507
1514 repository = relationship('Repository')
1508 repository = relationship('Repository')
1515
1509
1516 @property
1510 @property
1517 def field_key_prefixed(self):
1511 def field_key_prefixed(self):
1518 return 'ex_%s' % self.field_key
1512 return 'ex_%s' % self.field_key
1519
1513
1520 @classmethod
1514 @classmethod
1521 def un_prefix_key(cls, key):
1515 def un_prefix_key(cls, key):
1522 if key.startswith(cls.PREFIX):
1516 if key.startswith(cls.PREFIX):
1523 return key[len(cls.PREFIX):]
1517 return key[len(cls.PREFIX):]
1524 return key
1518 return key
1525
1519
1526 @classmethod
1520 @classmethod
1527 def get_by_key_name(cls, key, repo):
1521 def get_by_key_name(cls, key, repo):
1528 row = cls.query()\
1522 row = cls.query()\
1529 .filter(cls.repository == repo)\
1523 .filter(cls.repository == repo)\
1530 .filter(cls.field_key == key).scalar()
1524 .filter(cls.field_key == key).scalar()
1531 return row
1525 return row
1532
1526
1533
1527
1534 class Repository(Base, BaseModel):
1528 class Repository(Base, BaseModel):
1535 __tablename__ = 'repositories'
1529 __tablename__ = 'repositories'
1536 __table_args__ = (
1530 __table_args__ = (
1537 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1531 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1538 base_table_args,
1532 base_table_args,
1539 )
1533 )
1540 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1534 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1541 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1535 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1542 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1536 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1543
1537
1544 STATE_CREATED = 'repo_state_created'
1538 STATE_CREATED = 'repo_state_created'
1545 STATE_PENDING = 'repo_state_pending'
1539 STATE_PENDING = 'repo_state_pending'
1546 STATE_ERROR = 'repo_state_error'
1540 STATE_ERROR = 'repo_state_error'
1547
1541
1548 LOCK_AUTOMATIC = 'lock_auto'
1542 LOCK_AUTOMATIC = 'lock_auto'
1549 LOCK_API = 'lock_api'
1543 LOCK_API = 'lock_api'
1550 LOCK_WEB = 'lock_web'
1544 LOCK_WEB = 'lock_web'
1551 LOCK_PULL = 'lock_pull'
1545 LOCK_PULL = 'lock_pull'
1552
1546
1553 NAME_SEP = URL_SEP
1547 NAME_SEP = URL_SEP
1554
1548
1555 repo_id = Column(
1549 repo_id = Column(
1556 "repo_id", Integer(), nullable=False, unique=True, default=None,
1550 "repo_id", Integer(), nullable=False, unique=True, default=None,
1557 primary_key=True)
1551 primary_key=True)
1558 _repo_name = Column(
1552 _repo_name = Column(
1559 "repo_name", Text(), nullable=False, default=None)
1553 "repo_name", Text(), nullable=False, default=None)
1560 _repo_name_hash = Column(
1554 _repo_name_hash = Column(
1561 "repo_name_hash", String(255), nullable=False, unique=True)
1555 "repo_name_hash", String(255), nullable=False, unique=True)
1562 repo_state = Column("repo_state", String(255), nullable=True)
1556 repo_state = Column("repo_state", String(255), nullable=True)
1563
1557
1564 clone_uri = Column(
1558 clone_uri = Column(
1565 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1559 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1566 default=None)
1560 default=None)
1567 push_uri = Column(
1561 push_uri = Column(
1568 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1562 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1569 default=None)
1563 default=None)
1570 repo_type = Column(
1564 repo_type = Column(
1571 "repo_type", String(255), nullable=False, unique=False, default=None)
1565 "repo_type", String(255), nullable=False, unique=False, default=None)
1572 user_id = Column(
1566 user_id = Column(
1573 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1567 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1574 unique=False, default=None)
1568 unique=False, default=None)
1575 private = Column(
1569 private = Column(
1576 "private", Boolean(), nullable=True, unique=None, default=None)
1570 "private", Boolean(), nullable=True, unique=None, default=None)
1577 enable_statistics = Column(
1571 enable_statistics = Column(
1578 "statistics", Boolean(), nullable=True, unique=None, default=True)
1572 "statistics", Boolean(), nullable=True, unique=None, default=True)
1579 enable_downloads = Column(
1573 enable_downloads = Column(
1580 "downloads", Boolean(), nullable=True, unique=None, default=True)
1574 "downloads", Boolean(), nullable=True, unique=None, default=True)
1581 description = Column(
1575 description = Column(
1582 "description", String(10000), nullable=True, unique=None, default=None)
1576 "description", String(10000), nullable=True, unique=None, default=None)
1583 created_on = Column(
1577 created_on = Column(
1584 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1578 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1585 default=datetime.datetime.now)
1579 default=datetime.datetime.now)
1586 updated_on = Column(
1580 updated_on = Column(
1587 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1581 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1588 default=datetime.datetime.now)
1582 default=datetime.datetime.now)
1589 _landing_revision = Column(
1583 _landing_revision = Column(
1590 "landing_revision", String(255), nullable=False, unique=False,
1584 "landing_revision", String(255), nullable=False, unique=False,
1591 default=None)
1585 default=None)
1592 enable_locking = Column(
1586 enable_locking = Column(
1593 "enable_locking", Boolean(), nullable=False, unique=None,
1587 "enable_locking", Boolean(), nullable=False, unique=None,
1594 default=False)
1588 default=False)
1595 _locked = Column(
1589 _locked = Column(
1596 "locked", String(255), nullable=True, unique=False, default=None)
1590 "locked", String(255), nullable=True, unique=False, default=None)
1597 _changeset_cache = Column(
1591 _changeset_cache = Column(
1598 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1592 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1599
1593
1600 fork_id = Column(
1594 fork_id = Column(
1601 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1595 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1602 nullable=True, unique=False, default=None)
1596 nullable=True, unique=False, default=None)
1603 group_id = Column(
1597 group_id = Column(
1604 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1598 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1605 unique=False, default=None)
1599 unique=False, default=None)
1606
1600
1607 user = relationship('User', lazy='joined')
1601 user = relationship('User', lazy='joined')
1608 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1602 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1609 group = relationship('RepoGroup', lazy='joined')
1603 group = relationship('RepoGroup', lazy='joined')
1610 repo_to_perm = relationship(
1604 repo_to_perm = relationship(
1611 'UserRepoToPerm', cascade='all',
1605 'UserRepoToPerm', cascade='all',
1612 order_by='UserRepoToPerm.repo_to_perm_id')
1606 order_by='UserRepoToPerm.repo_to_perm_id')
1613 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1607 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1614 stats = relationship('Statistics', cascade='all', uselist=False)
1608 stats = relationship('Statistics', cascade='all', uselist=False)
1615
1609
1616 followers = relationship(
1610 followers = relationship(
1617 'UserFollowing',
1611 'UserFollowing',
1618 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1612 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1619 cascade='all')
1613 cascade='all')
1620 extra_fields = relationship(
1614 extra_fields = relationship(
1621 'RepositoryField', cascade="all, delete, delete-orphan")
1615 'RepositoryField', cascade="all, delete, delete-orphan")
1622 logs = relationship('UserLog')
1616 logs = relationship('UserLog')
1623 comments = relationship(
1617 comments = relationship(
1624 'ChangesetComment', cascade="all, delete, delete-orphan")
1618 'ChangesetComment', cascade="all, delete, delete-orphan")
1625 pull_requests_source = relationship(
1619 pull_requests_source = relationship(
1626 'PullRequest',
1620 'PullRequest',
1627 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1621 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1628 cascade="all, delete, delete-orphan")
1622 cascade="all, delete, delete-orphan")
1629 pull_requests_target = relationship(
1623 pull_requests_target = relationship(
1630 'PullRequest',
1624 'PullRequest',
1631 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1625 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1632 cascade="all, delete, delete-orphan")
1626 cascade="all, delete, delete-orphan")
1633 ui = relationship('RepoRhodeCodeUi', cascade="all")
1627 ui = relationship('RepoRhodeCodeUi', cascade="all")
1634 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1628 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1635 integrations = relationship('Integration',
1629 integrations = relationship('Integration',
1636 cascade="all, delete, delete-orphan")
1630 cascade="all, delete, delete-orphan")
1637
1631
1638 scoped_tokens = relationship('UserApiKeys', cascade="all")
1632 scoped_tokens = relationship('UserApiKeys', cascade="all")
1639
1633
1640 def __unicode__(self):
1634 def __unicode__(self):
1641 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1635 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1642 safe_unicode(self.repo_name))
1636 safe_unicode(self.repo_name))
1643
1637
1644 @hybrid_property
1638 @hybrid_property
1645 def description_safe(self):
1639 def description_safe(self):
1646 from rhodecode.lib import helpers as h
1640 from rhodecode.lib import helpers as h
1647 return h.escape(self.description)
1641 return h.escape(self.description)
1648
1642
1649 @hybrid_property
1643 @hybrid_property
1650 def landing_rev(self):
1644 def landing_rev(self):
1651 # always should return [rev_type, rev]
1645 # always should return [rev_type, rev]
1652 if self._landing_revision:
1646 if self._landing_revision:
1653 _rev_info = self._landing_revision.split(':')
1647 _rev_info = self._landing_revision.split(':')
1654 if len(_rev_info) < 2:
1648 if len(_rev_info) < 2:
1655 _rev_info.insert(0, 'rev')
1649 _rev_info.insert(0, 'rev')
1656 return [_rev_info[0], _rev_info[1]]
1650 return [_rev_info[0], _rev_info[1]]
1657 return [None, None]
1651 return [None, None]
1658
1652
1659 @landing_rev.setter
1653 @landing_rev.setter
1660 def landing_rev(self, val):
1654 def landing_rev(self, val):
1661 if ':' not in val:
1655 if ':' not in val:
1662 raise ValueError('value must be delimited with `:` and consist '
1656 raise ValueError('value must be delimited with `:` and consist '
1663 'of <rev_type>:<rev>, got %s instead' % val)
1657 'of <rev_type>:<rev>, got %s instead' % val)
1664 self._landing_revision = val
1658 self._landing_revision = val
1665
1659
1666 @hybrid_property
1660 @hybrid_property
1667 def locked(self):
1661 def locked(self):
1668 if self._locked:
1662 if self._locked:
1669 user_id, timelocked, reason = self._locked.split(':')
1663 user_id, timelocked, reason = self._locked.split(':')
1670 lock_values = int(user_id), timelocked, reason
1664 lock_values = int(user_id), timelocked, reason
1671 else:
1665 else:
1672 lock_values = [None, None, None]
1666 lock_values = [None, None, None]
1673 return lock_values
1667 return lock_values
1674
1668
1675 @locked.setter
1669 @locked.setter
1676 def locked(self, val):
1670 def locked(self, val):
1677 if val and isinstance(val, (list, tuple)):
1671 if val and isinstance(val, (list, tuple)):
1678 self._locked = ':'.join(map(str, val))
1672 self._locked = ':'.join(map(str, val))
1679 else:
1673 else:
1680 self._locked = None
1674 self._locked = None
1681
1675
1682 @hybrid_property
1676 @hybrid_property
1683 def changeset_cache(self):
1677 def changeset_cache(self):
1684 from rhodecode.lib.vcs.backends.base import EmptyCommit
1678 from rhodecode.lib.vcs.backends.base import EmptyCommit
1685 dummy = EmptyCommit().__json__()
1679 dummy = EmptyCommit().__json__()
1686 if not self._changeset_cache:
1680 if not self._changeset_cache:
1687 return dummy
1681 return dummy
1688 try:
1682 try:
1689 return json.loads(self._changeset_cache)
1683 return json.loads(self._changeset_cache)
1690 except TypeError:
1684 except TypeError:
1691 return dummy
1685 return dummy
1692 except Exception:
1686 except Exception:
1693 log.error(traceback.format_exc())
1687 log.error(traceback.format_exc())
1694 return dummy
1688 return dummy
1695
1689
1696 @changeset_cache.setter
1690 @changeset_cache.setter
1697 def changeset_cache(self, val):
1691 def changeset_cache(self, val):
1698 try:
1692 try:
1699 self._changeset_cache = json.dumps(val)
1693 self._changeset_cache = json.dumps(val)
1700 except Exception:
1694 except Exception:
1701 log.error(traceback.format_exc())
1695 log.error(traceback.format_exc())
1702
1696
1703 @hybrid_property
1697 @hybrid_property
1704 def repo_name(self):
1698 def repo_name(self):
1705 return self._repo_name
1699 return self._repo_name
1706
1700
1707 @repo_name.setter
1701 @repo_name.setter
1708 def repo_name(self, value):
1702 def repo_name(self, value):
1709 self._repo_name = value
1703 self._repo_name = value
1710 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1704 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1711
1705
1712 @classmethod
1706 @classmethod
1713 def normalize_repo_name(cls, repo_name):
1707 def normalize_repo_name(cls, repo_name):
1714 """
1708 """
1715 Normalizes os specific repo_name to the format internally stored inside
1709 Normalizes os specific repo_name to the format internally stored inside
1716 database using URL_SEP
1710 database using URL_SEP
1717
1711
1718 :param cls:
1712 :param cls:
1719 :param repo_name:
1713 :param repo_name:
1720 """
1714 """
1721 return cls.NAME_SEP.join(repo_name.split(os.sep))
1715 return cls.NAME_SEP.join(repo_name.split(os.sep))
1722
1716
1723 @classmethod
1717 @classmethod
1724 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1718 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1725 session = Session()
1719 session = Session()
1726 q = session.query(cls).filter(cls.repo_name == repo_name)
1720 q = session.query(cls).filter(cls.repo_name == repo_name)
1727
1721
1728 if cache:
1722 if cache:
1729 if identity_cache:
1723 if identity_cache:
1730 val = cls.identity_cache(session, 'repo_name', repo_name)
1724 val = cls.identity_cache(session, 'repo_name', repo_name)
1731 if val:
1725 if val:
1732 return val
1726 return val
1733 else:
1727 else:
1734 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1728 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1735 q = q.options(
1729 q = q.options(
1736 FromCache("sql_cache_short", cache_key))
1730 FromCache("sql_cache_short", cache_key))
1737
1731
1738 return q.scalar()
1732 return q.scalar()
1739
1733
1740 @classmethod
1734 @classmethod
1741 def get_by_id_or_repo_name(cls, repoid):
1735 def get_by_id_or_repo_name(cls, repoid):
1742 if isinstance(repoid, (int, long)):
1736 if isinstance(repoid, (int, long)):
1743 try:
1737 try:
1744 repo = cls.get(repoid)
1738 repo = cls.get(repoid)
1745 except ValueError:
1739 except ValueError:
1746 repo = None
1740 repo = None
1747 else:
1741 else:
1748 repo = cls.get_by_repo_name(repoid)
1742 repo = cls.get_by_repo_name(repoid)
1749 return repo
1743 return repo
1750
1744
1751 @classmethod
1745 @classmethod
1752 def get_by_full_path(cls, repo_full_path):
1746 def get_by_full_path(cls, repo_full_path):
1753 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1747 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1754 repo_name = cls.normalize_repo_name(repo_name)
1748 repo_name = cls.normalize_repo_name(repo_name)
1755 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1749 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1756
1750
1757 @classmethod
1751 @classmethod
1758 def get_repo_forks(cls, repo_id):
1752 def get_repo_forks(cls, repo_id):
1759 return cls.query().filter(Repository.fork_id == repo_id)
1753 return cls.query().filter(Repository.fork_id == repo_id)
1760
1754
1761 @classmethod
1755 @classmethod
1762 def base_path(cls):
1756 def base_path(cls):
1763 """
1757 """
1764 Returns base path when all repos are stored
1758 Returns base path when all repos are stored
1765
1759
1766 :param cls:
1760 :param cls:
1767 """
1761 """
1768 q = Session().query(RhodeCodeUi)\
1762 q = Session().query(RhodeCodeUi)\
1769 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1763 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1770 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1764 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1771 return q.one().ui_value
1765 return q.one().ui_value
1772
1766
1773 @classmethod
1767 @classmethod
1774 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1768 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1775 case_insensitive=True):
1769 case_insensitive=True):
1776 q = Repository.query()
1770 q = Repository.query()
1777
1771
1778 if not isinstance(user_id, Optional):
1772 if not isinstance(user_id, Optional):
1779 q = q.filter(Repository.user_id == user_id)
1773 q = q.filter(Repository.user_id == user_id)
1780
1774
1781 if not isinstance(group_id, Optional):
1775 if not isinstance(group_id, Optional):
1782 q = q.filter(Repository.group_id == group_id)
1776 q = q.filter(Repository.group_id == group_id)
1783
1777
1784 if case_insensitive:
1778 if case_insensitive:
1785 q = q.order_by(func.lower(Repository.repo_name))
1779 q = q.order_by(func.lower(Repository.repo_name))
1786 else:
1780 else:
1787 q = q.order_by(Repository.repo_name)
1781 q = q.order_by(Repository.repo_name)
1788 return q.all()
1782 return q.all()
1789
1783
1790 @property
1784 @property
1791 def forks(self):
1785 def forks(self):
1792 """
1786 """
1793 Return forks of this repo
1787 Return forks of this repo
1794 """
1788 """
1795 return Repository.get_repo_forks(self.repo_id)
1789 return Repository.get_repo_forks(self.repo_id)
1796
1790
1797 @property
1791 @property
1798 def parent(self):
1792 def parent(self):
1799 """
1793 """
1800 Returns fork parent
1794 Returns fork parent
1801 """
1795 """
1802 return self.fork
1796 return self.fork
1803
1797
1804 @property
1798 @property
1805 def just_name(self):
1799 def just_name(self):
1806 return self.repo_name.split(self.NAME_SEP)[-1]
1800 return self.repo_name.split(self.NAME_SEP)[-1]
1807
1801
1808 @property
1802 @property
1809 def groups_with_parents(self):
1803 def groups_with_parents(self):
1810 groups = []
1804 groups = []
1811 if self.group is None:
1805 if self.group is None:
1812 return groups
1806 return groups
1813
1807
1814 cur_gr = self.group
1808 cur_gr = self.group
1815 groups.insert(0, cur_gr)
1809 groups.insert(0, cur_gr)
1816 while 1:
1810 while 1:
1817 gr = getattr(cur_gr, 'parent_group', None)
1811 gr = getattr(cur_gr, 'parent_group', None)
1818 cur_gr = cur_gr.parent_group
1812 cur_gr = cur_gr.parent_group
1819 if gr is None:
1813 if gr is None:
1820 break
1814 break
1821 groups.insert(0, gr)
1815 groups.insert(0, gr)
1822
1816
1823 return groups
1817 return groups
1824
1818
1825 @property
1819 @property
1826 def groups_and_repo(self):
1820 def groups_and_repo(self):
1827 return self.groups_with_parents, self
1821 return self.groups_with_parents, self
1828
1822
1829 @LazyProperty
1823 @LazyProperty
1830 def repo_path(self):
1824 def repo_path(self):
1831 """
1825 """
1832 Returns base full path for that repository means where it actually
1826 Returns base full path for that repository means where it actually
1833 exists on a filesystem
1827 exists on a filesystem
1834 """
1828 """
1835 q = Session().query(RhodeCodeUi).filter(
1829 q = Session().query(RhodeCodeUi).filter(
1836 RhodeCodeUi.ui_key == self.NAME_SEP)
1830 RhodeCodeUi.ui_key == self.NAME_SEP)
1837 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1831 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1838 return q.one().ui_value
1832 return q.one().ui_value
1839
1833
1840 @property
1834 @property
1841 def repo_full_path(self):
1835 def repo_full_path(self):
1842 p = [self.repo_path]
1836 p = [self.repo_path]
1843 # we need to split the name by / since this is how we store the
1837 # we need to split the name by / since this is how we store the
1844 # names in the database, but that eventually needs to be converted
1838 # names in the database, but that eventually needs to be converted
1845 # into a valid system path
1839 # into a valid system path
1846 p += self.repo_name.split(self.NAME_SEP)
1840 p += self.repo_name.split(self.NAME_SEP)
1847 return os.path.join(*map(safe_unicode, p))
1841 return os.path.join(*map(safe_unicode, p))
1848
1842
1849 @property
1843 @property
1850 def cache_keys(self):
1844 def cache_keys(self):
1851 """
1845 """
1852 Returns associated cache keys for that repo
1846 Returns associated cache keys for that repo
1853 """
1847 """
1854 return CacheKey.query()\
1848 return CacheKey.query()\
1855 .filter(CacheKey.cache_args == self.repo_name)\
1849 .filter(CacheKey.cache_args == self.repo_name)\
1856 .order_by(CacheKey.cache_key)\
1850 .order_by(CacheKey.cache_key)\
1857 .all()
1851 .all()
1858
1852
1859 @property
1853 @property
1860 def cached_diffs_relative_dir(self):
1854 def cached_diffs_relative_dir(self):
1861 """
1855 """
1862 Return a relative to the repository store path of cached diffs
1856 Return a relative to the repository store path of cached diffs
1863 used for safe display for users, who shouldn't know the absolute store
1857 used for safe display for users, who shouldn't know the absolute store
1864 path
1858 path
1865 """
1859 """
1866 return os.path.join(
1860 return os.path.join(
1867 os.path.dirname(self.repo_name),
1861 os.path.dirname(self.repo_name),
1868 self.cached_diffs_dir.split(os.path.sep)[-1])
1862 self.cached_diffs_dir.split(os.path.sep)[-1])
1869
1863
1870 @property
1864 @property
1871 def cached_diffs_dir(self):
1865 def cached_diffs_dir(self):
1872 path = self.repo_full_path
1866 path = self.repo_full_path
1873 return os.path.join(
1867 return os.path.join(
1874 os.path.dirname(path),
1868 os.path.dirname(path),
1875 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1869 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1876
1870
1877 def cached_diffs(self):
1871 def cached_diffs(self):
1878 diff_cache_dir = self.cached_diffs_dir
1872 diff_cache_dir = self.cached_diffs_dir
1879 if os.path.isdir(diff_cache_dir):
1873 if os.path.isdir(diff_cache_dir):
1880 return os.listdir(diff_cache_dir)
1874 return os.listdir(diff_cache_dir)
1881 return []
1875 return []
1882
1876
1883 def shadow_repos(self):
1877 def shadow_repos(self):
1884 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1878 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1885 return [
1879 return [
1886 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1880 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1887 if x.startswith(shadow_repos_pattern)]
1881 if x.startswith(shadow_repos_pattern)]
1888
1882
1889 def get_new_name(self, repo_name):
1883 def get_new_name(self, repo_name):
1890 """
1884 """
1891 returns new full repository name based on assigned group and new new
1885 returns new full repository name based on assigned group and new new
1892
1886
1893 :param group_name:
1887 :param group_name:
1894 """
1888 """
1895 path_prefix = self.group.full_path_splitted if self.group else []
1889 path_prefix = self.group.full_path_splitted if self.group else []
1896 return self.NAME_SEP.join(path_prefix + [repo_name])
1890 return self.NAME_SEP.join(path_prefix + [repo_name])
1897
1891
1898 @property
1892 @property
1899 def _config(self):
1893 def _config(self):
1900 """
1894 """
1901 Returns db based config object.
1895 Returns db based config object.
1902 """
1896 """
1903 from rhodecode.lib.utils import make_db_config
1897 from rhodecode.lib.utils import make_db_config
1904 return make_db_config(clear_session=False, repo=self)
1898 return make_db_config(clear_session=False, repo=self)
1905
1899
1906 def permissions(self, with_admins=True, with_owner=True):
1900 def permissions(self, with_admins=True, with_owner=True):
1907 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1901 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1908 q = q.options(joinedload(UserRepoToPerm.repository),
1902 q = q.options(joinedload(UserRepoToPerm.repository),
1909 joinedload(UserRepoToPerm.user),
1903 joinedload(UserRepoToPerm.user),
1910 joinedload(UserRepoToPerm.permission),)
1904 joinedload(UserRepoToPerm.permission),)
1911
1905
1912 # get owners and admins and permissions. We do a trick of re-writing
1906 # get owners and admins and permissions. We do a trick of re-writing
1913 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1907 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1914 # has a global reference and changing one object propagates to all
1908 # has a global reference and changing one object propagates to all
1915 # others. This means if admin is also an owner admin_row that change
1909 # others. This means if admin is also an owner admin_row that change
1916 # would propagate to both objects
1910 # would propagate to both objects
1917 perm_rows = []
1911 perm_rows = []
1918 for _usr in q.all():
1912 for _usr in q.all():
1919 usr = AttributeDict(_usr.user.get_dict())
1913 usr = AttributeDict(_usr.user.get_dict())
1920 usr.permission = _usr.permission.permission_name
1914 usr.permission = _usr.permission.permission_name
1921 perm_rows.append(usr)
1915 perm_rows.append(usr)
1922
1916
1923 # filter the perm rows by 'default' first and then sort them by
1917 # filter the perm rows by 'default' first and then sort them by
1924 # admin,write,read,none permissions sorted again alphabetically in
1918 # admin,write,read,none permissions sorted again alphabetically in
1925 # each group
1919 # each group
1926 perm_rows = sorted(perm_rows, key=display_user_sort)
1920 perm_rows = sorted(perm_rows, key=display_user_sort)
1927
1921
1928 _admin_perm = 'repository.admin'
1922 _admin_perm = 'repository.admin'
1929 owner_row = []
1923 owner_row = []
1930 if with_owner:
1924 if with_owner:
1931 usr = AttributeDict(self.user.get_dict())
1925 usr = AttributeDict(self.user.get_dict())
1932 usr.owner_row = True
1926 usr.owner_row = True
1933 usr.permission = _admin_perm
1927 usr.permission = _admin_perm
1934 owner_row.append(usr)
1928 owner_row.append(usr)
1935
1929
1936 super_admin_rows = []
1930 super_admin_rows = []
1937 if with_admins:
1931 if with_admins:
1938 for usr in User.get_all_super_admins():
1932 for usr in User.get_all_super_admins():
1939 # if this admin is also owner, don't double the record
1933 # if this admin is also owner, don't double the record
1940 if usr.user_id == owner_row[0].user_id:
1934 if usr.user_id == owner_row[0].user_id:
1941 owner_row[0].admin_row = True
1935 owner_row[0].admin_row = True
1942 else:
1936 else:
1943 usr = AttributeDict(usr.get_dict())
1937 usr = AttributeDict(usr.get_dict())
1944 usr.admin_row = True
1938 usr.admin_row = True
1945 usr.permission = _admin_perm
1939 usr.permission = _admin_perm
1946 super_admin_rows.append(usr)
1940 super_admin_rows.append(usr)
1947
1941
1948 return super_admin_rows + owner_row + perm_rows
1942 return super_admin_rows + owner_row + perm_rows
1949
1943
1950 def permission_user_groups(self):
1944 def permission_user_groups(self):
1951 q = UserGroupRepoToPerm.query().filter(
1945 q = UserGroupRepoToPerm.query().filter(
1952 UserGroupRepoToPerm.repository == self)
1946 UserGroupRepoToPerm.repository == self)
1953 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1947 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1954 joinedload(UserGroupRepoToPerm.users_group),
1948 joinedload(UserGroupRepoToPerm.users_group),
1955 joinedload(UserGroupRepoToPerm.permission),)
1949 joinedload(UserGroupRepoToPerm.permission),)
1956
1950
1957 perm_rows = []
1951 perm_rows = []
1958 for _user_group in q.all():
1952 for _user_group in q.all():
1959 usr = AttributeDict(_user_group.users_group.get_dict())
1953 usr = AttributeDict(_user_group.users_group.get_dict())
1960 usr.permission = _user_group.permission.permission_name
1954 usr.permission = _user_group.permission.permission_name
1961 perm_rows.append(usr)
1955 perm_rows.append(usr)
1962
1956
1963 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1957 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1964 return perm_rows
1958 return perm_rows
1965
1959
1966 def get_api_data(self, include_secrets=False):
1960 def get_api_data(self, include_secrets=False):
1967 """
1961 """
1968 Common function for generating repo api data
1962 Common function for generating repo api data
1969
1963
1970 :param include_secrets: See :meth:`User.get_api_data`.
1964 :param include_secrets: See :meth:`User.get_api_data`.
1971
1965
1972 """
1966 """
1973 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1967 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1974 # move this methods on models level.
1968 # move this methods on models level.
1975 from rhodecode.model.settings import SettingsModel
1969 from rhodecode.model.settings import SettingsModel
1976 from rhodecode.model.repo import RepoModel
1970 from rhodecode.model.repo import RepoModel
1977
1971
1978 repo = self
1972 repo = self
1979 _user_id, _time, _reason = self.locked
1973 _user_id, _time, _reason = self.locked
1980
1974
1981 data = {
1975 data = {
1982 'repo_id': repo.repo_id,
1976 'repo_id': repo.repo_id,
1983 'repo_name': repo.repo_name,
1977 'repo_name': repo.repo_name,
1984 'repo_type': repo.repo_type,
1978 'repo_type': repo.repo_type,
1985 'clone_uri': repo.clone_uri or '',
1979 'clone_uri': repo.clone_uri or '',
1986 'push_uri': repo.push_uri or '',
1980 'push_uri': repo.push_uri or '',
1987 'url': RepoModel().get_url(self),
1981 'url': RepoModel().get_url(self),
1988 'private': repo.private,
1982 'private': repo.private,
1989 'created_on': repo.created_on,
1983 'created_on': repo.created_on,
1990 'description': repo.description_safe,
1984 'description': repo.description_safe,
1991 'landing_rev': repo.landing_rev,
1985 'landing_rev': repo.landing_rev,
1992 'owner': repo.user.username,
1986 'owner': repo.user.username,
1993 'fork_of': repo.fork.repo_name if repo.fork else None,
1987 'fork_of': repo.fork.repo_name if repo.fork else None,
1994 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1988 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1995 'enable_statistics': repo.enable_statistics,
1989 'enable_statistics': repo.enable_statistics,
1996 'enable_locking': repo.enable_locking,
1990 'enable_locking': repo.enable_locking,
1997 'enable_downloads': repo.enable_downloads,
1991 'enable_downloads': repo.enable_downloads,
1998 'last_changeset': repo.changeset_cache,
1992 'last_changeset': repo.changeset_cache,
1999 'locked_by': User.get(_user_id).get_api_data(
1993 'locked_by': User.get(_user_id).get_api_data(
2000 include_secrets=include_secrets) if _user_id else None,
1994 include_secrets=include_secrets) if _user_id else None,
2001 'locked_date': time_to_datetime(_time) if _time else None,
1995 'locked_date': time_to_datetime(_time) if _time else None,
2002 'lock_reason': _reason if _reason else None,
1996 'lock_reason': _reason if _reason else None,
2003 }
1997 }
2004
1998
2005 # TODO: mikhail: should be per-repo settings here
1999 # TODO: mikhail: should be per-repo settings here
2006 rc_config = SettingsModel().get_all_settings()
2000 rc_config = SettingsModel().get_all_settings()
2007 repository_fields = str2bool(
2001 repository_fields = str2bool(
2008 rc_config.get('rhodecode_repository_fields'))
2002 rc_config.get('rhodecode_repository_fields'))
2009 if repository_fields:
2003 if repository_fields:
2010 for f in self.extra_fields:
2004 for f in self.extra_fields:
2011 data[f.field_key_prefixed] = f.field_value
2005 data[f.field_key_prefixed] = f.field_value
2012
2006
2013 return data
2007 return data
2014
2008
2015 @classmethod
2009 @classmethod
2016 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2010 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2017 if not lock_time:
2011 if not lock_time:
2018 lock_time = time.time()
2012 lock_time = time.time()
2019 if not lock_reason:
2013 if not lock_reason:
2020 lock_reason = cls.LOCK_AUTOMATIC
2014 lock_reason = cls.LOCK_AUTOMATIC
2021 repo.locked = [user_id, lock_time, lock_reason]
2015 repo.locked = [user_id, lock_time, lock_reason]
2022 Session().add(repo)
2016 Session().add(repo)
2023 Session().commit()
2017 Session().commit()
2024
2018
2025 @classmethod
2019 @classmethod
2026 def unlock(cls, repo):
2020 def unlock(cls, repo):
2027 repo.locked = None
2021 repo.locked = None
2028 Session().add(repo)
2022 Session().add(repo)
2029 Session().commit()
2023 Session().commit()
2030
2024
2031 @classmethod
2025 @classmethod
2032 def getlock(cls, repo):
2026 def getlock(cls, repo):
2033 return repo.locked
2027 return repo.locked
2034
2028
2035 def is_user_lock(self, user_id):
2029 def is_user_lock(self, user_id):
2036 if self.lock[0]:
2030 if self.lock[0]:
2037 lock_user_id = safe_int(self.lock[0])
2031 lock_user_id = safe_int(self.lock[0])
2038 user_id = safe_int(user_id)
2032 user_id = safe_int(user_id)
2039 # both are ints, and they are equal
2033 # both are ints, and they are equal
2040 return all([lock_user_id, user_id]) and lock_user_id == user_id
2034 return all([lock_user_id, user_id]) and lock_user_id == user_id
2041
2035
2042 return False
2036 return False
2043
2037
2044 def get_locking_state(self, action, user_id, only_when_enabled=True):
2038 def get_locking_state(self, action, user_id, only_when_enabled=True):
2045 """
2039 """
2046 Checks locking on this repository, if locking is enabled and lock is
2040 Checks locking on this repository, if locking is enabled and lock is
2047 present returns a tuple of make_lock, locked, locked_by.
2041 present returns a tuple of make_lock, locked, locked_by.
2048 make_lock can have 3 states None (do nothing) True, make lock
2042 make_lock can have 3 states None (do nothing) True, make lock
2049 False release lock, This value is later propagated to hooks, which
2043 False release lock, This value is later propagated to hooks, which
2050 do the locking. Think about this as signals passed to hooks what to do.
2044 do the locking. Think about this as signals passed to hooks what to do.
2051
2045
2052 """
2046 """
2053 # TODO: johbo: This is part of the business logic and should be moved
2047 # TODO: johbo: This is part of the business logic and should be moved
2054 # into the RepositoryModel.
2048 # into the RepositoryModel.
2055
2049
2056 if action not in ('push', 'pull'):
2050 if action not in ('push', 'pull'):
2057 raise ValueError("Invalid action value: %s" % repr(action))
2051 raise ValueError("Invalid action value: %s" % repr(action))
2058
2052
2059 # defines if locked error should be thrown to user
2053 # defines if locked error should be thrown to user
2060 currently_locked = False
2054 currently_locked = False
2061 # defines if new lock should be made, tri-state
2055 # defines if new lock should be made, tri-state
2062 make_lock = None
2056 make_lock = None
2063 repo = self
2057 repo = self
2064 user = User.get(user_id)
2058 user = User.get(user_id)
2065
2059
2066 lock_info = repo.locked
2060 lock_info = repo.locked
2067
2061
2068 if repo and (repo.enable_locking or not only_when_enabled):
2062 if repo and (repo.enable_locking or not only_when_enabled):
2069 if action == 'push':
2063 if action == 'push':
2070 # check if it's already locked !, if it is compare users
2064 # check if it's already locked !, if it is compare users
2071 locked_by_user_id = lock_info[0]
2065 locked_by_user_id = lock_info[0]
2072 if user.user_id == locked_by_user_id:
2066 if user.user_id == locked_by_user_id:
2073 log.debug(
2067 log.debug(
2074 'Got `push` action from user %s, now unlocking', user)
2068 'Got `push` action from user %s, now unlocking', user)
2075 # unlock if we have push from user who locked
2069 # unlock if we have push from user who locked
2076 make_lock = False
2070 make_lock = False
2077 else:
2071 else:
2078 # we're not the same user who locked, ban with
2072 # we're not the same user who locked, ban with
2079 # code defined in settings (default is 423 HTTP Locked) !
2073 # code defined in settings (default is 423 HTTP Locked) !
2080 log.debug('Repo %s is currently locked by %s', repo, user)
2074 log.debug('Repo %s is currently locked by %s', repo, user)
2081 currently_locked = True
2075 currently_locked = True
2082 elif action == 'pull':
2076 elif action == 'pull':
2083 # [0] user [1] date
2077 # [0] user [1] date
2084 if lock_info[0] and lock_info[1]:
2078 if lock_info[0] and lock_info[1]:
2085 log.debug('Repo %s is currently locked by %s', repo, user)
2079 log.debug('Repo %s is currently locked by %s', repo, user)
2086 currently_locked = True
2080 currently_locked = True
2087 else:
2081 else:
2088 log.debug('Setting lock on repo %s by %s', repo, user)
2082 log.debug('Setting lock on repo %s by %s', repo, user)
2089 make_lock = True
2083 make_lock = True
2090
2084
2091 else:
2085 else:
2092 log.debug('Repository %s do not have locking enabled', repo)
2086 log.debug('Repository %s do not have locking enabled', repo)
2093
2087
2094 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2088 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2095 make_lock, currently_locked, lock_info)
2089 make_lock, currently_locked, lock_info)
2096
2090
2097 from rhodecode.lib.auth import HasRepoPermissionAny
2091 from rhodecode.lib.auth import HasRepoPermissionAny
2098 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2092 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2099 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2093 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2100 # if we don't have at least write permission we cannot make a lock
2094 # if we don't have at least write permission we cannot make a lock
2101 log.debug('lock state reset back to FALSE due to lack '
2095 log.debug('lock state reset back to FALSE due to lack '
2102 'of at least read permission')
2096 'of at least read permission')
2103 make_lock = False
2097 make_lock = False
2104
2098
2105 return make_lock, currently_locked, lock_info
2099 return make_lock, currently_locked, lock_info
2106
2100
2107 @property
2101 @property
2108 def last_db_change(self):
2102 def last_db_change(self):
2109 return self.updated_on
2103 return self.updated_on
2110
2104
2111 @property
2105 @property
2112 def clone_uri_hidden(self):
2106 def clone_uri_hidden(self):
2113 clone_uri = self.clone_uri
2107 clone_uri = self.clone_uri
2114 if clone_uri:
2108 if clone_uri:
2115 import urlobject
2109 import urlobject
2116 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2110 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2117 if url_obj.password:
2111 if url_obj.password:
2118 clone_uri = url_obj.with_password('*****')
2112 clone_uri = url_obj.with_password('*****')
2119 return clone_uri
2113 return clone_uri
2120
2114
2121 @property
2115 @property
2122 def push_uri_hidden(self):
2116 def push_uri_hidden(self):
2123 push_uri = self.push_uri
2117 push_uri = self.push_uri
2124 if push_uri:
2118 if push_uri:
2125 import urlobject
2119 import urlobject
2126 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2120 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2127 if url_obj.password:
2121 if url_obj.password:
2128 push_uri = url_obj.with_password('*****')
2122 push_uri = url_obj.with_password('*****')
2129 return push_uri
2123 return push_uri
2130
2124
2131 def clone_url(self, **override):
2125 def clone_url(self, **override):
2132 from rhodecode.model.settings import SettingsModel
2126 from rhodecode.model.settings import SettingsModel
2133
2127
2134 uri_tmpl = None
2128 uri_tmpl = None
2135 if 'with_id' in override:
2129 if 'with_id' in override:
2136 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2130 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2137 del override['with_id']
2131 del override['with_id']
2138
2132
2139 if 'uri_tmpl' in override:
2133 if 'uri_tmpl' in override:
2140 uri_tmpl = override['uri_tmpl']
2134 uri_tmpl = override['uri_tmpl']
2141 del override['uri_tmpl']
2135 del override['uri_tmpl']
2142
2136
2143 ssh = False
2137 ssh = False
2144 if 'ssh' in override:
2138 if 'ssh' in override:
2145 ssh = True
2139 ssh = True
2146 del override['ssh']
2140 del override['ssh']
2147
2141
2148 # we didn't override our tmpl from **overrides
2142 # we didn't override our tmpl from **overrides
2149 if not uri_tmpl:
2143 if not uri_tmpl:
2150 rc_config = SettingsModel().get_all_settings(cache=True)
2144 rc_config = SettingsModel().get_all_settings(cache=True)
2151 if ssh:
2145 if ssh:
2152 uri_tmpl = rc_config.get(
2146 uri_tmpl = rc_config.get(
2153 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2147 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2154 else:
2148 else:
2155 uri_tmpl = rc_config.get(
2149 uri_tmpl = rc_config.get(
2156 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2150 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2157
2151
2158 request = get_current_request()
2152 request = get_current_request()
2159 return get_clone_url(request=request,
2153 return get_clone_url(request=request,
2160 uri_tmpl=uri_tmpl,
2154 uri_tmpl=uri_tmpl,
2161 repo_name=self.repo_name,
2155 repo_name=self.repo_name,
2162 repo_id=self.repo_id, **override)
2156 repo_id=self.repo_id, **override)
2163
2157
2164 def set_state(self, state):
2158 def set_state(self, state):
2165 self.repo_state = state
2159 self.repo_state = state
2166 Session().add(self)
2160 Session().add(self)
2167 #==========================================================================
2161 #==========================================================================
2168 # SCM PROPERTIES
2162 # SCM PROPERTIES
2169 #==========================================================================
2163 #==========================================================================
2170
2164
2171 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2165 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2172 return get_commit_safe(
2166 return get_commit_safe(
2173 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2167 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2174
2168
2175 def get_changeset(self, rev=None, pre_load=None):
2169 def get_changeset(self, rev=None, pre_load=None):
2176 warnings.warn("Use get_commit", DeprecationWarning)
2170 warnings.warn("Use get_commit", DeprecationWarning)
2177 commit_id = None
2171 commit_id = None
2178 commit_idx = None
2172 commit_idx = None
2179 if isinstance(rev, basestring):
2173 if isinstance(rev, basestring):
2180 commit_id = rev
2174 commit_id = rev
2181 else:
2175 else:
2182 commit_idx = rev
2176 commit_idx = rev
2183 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2177 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2184 pre_load=pre_load)
2178 pre_load=pre_load)
2185
2179
2186 def get_landing_commit(self):
2180 def get_landing_commit(self):
2187 """
2181 """
2188 Returns landing commit, or if that doesn't exist returns the tip
2182 Returns landing commit, or if that doesn't exist returns the tip
2189 """
2183 """
2190 _rev_type, _rev = self.landing_rev
2184 _rev_type, _rev = self.landing_rev
2191 commit = self.get_commit(_rev)
2185 commit = self.get_commit(_rev)
2192 if isinstance(commit, EmptyCommit):
2186 if isinstance(commit, EmptyCommit):
2193 return self.get_commit()
2187 return self.get_commit()
2194 return commit
2188 return commit
2195
2189
2196 def update_commit_cache(self, cs_cache=None, config=None):
2190 def update_commit_cache(self, cs_cache=None, config=None):
2197 """
2191 """
2198 Update cache of last changeset for repository, keys should be::
2192 Update cache of last changeset for repository, keys should be::
2199
2193
2200 short_id
2194 short_id
2201 raw_id
2195 raw_id
2202 revision
2196 revision
2203 parents
2197 parents
2204 message
2198 message
2205 date
2199 date
2206 author
2200 author
2207
2201
2208 :param cs_cache:
2202 :param cs_cache:
2209 """
2203 """
2210 from rhodecode.lib.vcs.backends.base import BaseChangeset
2204 from rhodecode.lib.vcs.backends.base import BaseChangeset
2211 if cs_cache is None:
2205 if cs_cache is None:
2212 # use no-cache version here
2206 # use no-cache version here
2213 scm_repo = self.scm_instance(cache=False, config=config)
2207 scm_repo = self.scm_instance(cache=False, config=config)
2214 if scm_repo:
2208 if scm_repo:
2215 cs_cache = scm_repo.get_commit(
2209 cs_cache = scm_repo.get_commit(
2216 pre_load=["author", "date", "message", "parents"])
2210 pre_load=["author", "date", "message", "parents"])
2217 else:
2211 else:
2218 cs_cache = EmptyCommit()
2212 cs_cache = EmptyCommit()
2219
2213
2220 if isinstance(cs_cache, BaseChangeset):
2214 if isinstance(cs_cache, BaseChangeset):
2221 cs_cache = cs_cache.__json__()
2215 cs_cache = cs_cache.__json__()
2222
2216
2223 def is_outdated(new_cs_cache):
2217 def is_outdated(new_cs_cache):
2224 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2218 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2225 new_cs_cache['revision'] != self.changeset_cache['revision']):
2219 new_cs_cache['revision'] != self.changeset_cache['revision']):
2226 return True
2220 return True
2227 return False
2221 return False
2228
2222
2229 # check if we have maybe already latest cached revision
2223 # check if we have maybe already latest cached revision
2230 if is_outdated(cs_cache) or not self.changeset_cache:
2224 if is_outdated(cs_cache) or not self.changeset_cache:
2231 _default = datetime.datetime.utcnow()
2225 _default = datetime.datetime.utcnow()
2232 last_change = cs_cache.get('date') or _default
2226 last_change = cs_cache.get('date') or _default
2233 if self.updated_on and self.updated_on > last_change:
2227 if self.updated_on and self.updated_on > last_change:
2234 # we check if last update is newer than the new value
2228 # we check if last update is newer than the new value
2235 # if yes, we use the current timestamp instead. Imagine you get
2229 # if yes, we use the current timestamp instead. Imagine you get
2236 # old commit pushed 1y ago, we'd set last update 1y to ago.
2230 # old commit pushed 1y ago, we'd set last update 1y to ago.
2237 last_change = _default
2231 last_change = _default
2238 log.debug('updated repo %s with new cs cache %s',
2232 log.debug('updated repo %s with new cs cache %s',
2239 self.repo_name, cs_cache)
2233 self.repo_name, cs_cache)
2240 self.updated_on = last_change
2234 self.updated_on = last_change
2241 self.changeset_cache = cs_cache
2235 self.changeset_cache = cs_cache
2242 Session().add(self)
2236 Session().add(self)
2243 Session().commit()
2237 Session().commit()
2244 else:
2238 else:
2245 log.debug('Skipping update_commit_cache for repo:`%s` '
2239 log.debug('Skipping update_commit_cache for repo:`%s` '
2246 'commit already with latest changes', self.repo_name)
2240 'commit already with latest changes', self.repo_name)
2247
2241
2248 @property
2242 @property
2249 def tip(self):
2243 def tip(self):
2250 return self.get_commit('tip')
2244 return self.get_commit('tip')
2251
2245
2252 @property
2246 @property
2253 def author(self):
2247 def author(self):
2254 return self.tip.author
2248 return self.tip.author
2255
2249
2256 @property
2250 @property
2257 def last_change(self):
2251 def last_change(self):
2258 return self.scm_instance().last_change
2252 return self.scm_instance().last_change
2259
2253
2260 def get_comments(self, revisions=None):
2254 def get_comments(self, revisions=None):
2261 """
2255 """
2262 Returns comments for this repository grouped by revisions
2256 Returns comments for this repository grouped by revisions
2263
2257
2264 :param revisions: filter query by revisions only
2258 :param revisions: filter query by revisions only
2265 """
2259 """
2266 cmts = ChangesetComment.query()\
2260 cmts = ChangesetComment.query()\
2267 .filter(ChangesetComment.repo == self)
2261 .filter(ChangesetComment.repo == self)
2268 if revisions:
2262 if revisions:
2269 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2263 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2270 grouped = collections.defaultdict(list)
2264 grouped = collections.defaultdict(list)
2271 for cmt in cmts.all():
2265 for cmt in cmts.all():
2272 grouped[cmt.revision].append(cmt)
2266 grouped[cmt.revision].append(cmt)
2273 return grouped
2267 return grouped
2274
2268
2275 def statuses(self, revisions=None):
2269 def statuses(self, revisions=None):
2276 """
2270 """
2277 Returns statuses for this repository
2271 Returns statuses for this repository
2278
2272
2279 :param revisions: list of revisions to get statuses for
2273 :param revisions: list of revisions to get statuses for
2280 """
2274 """
2281 statuses = ChangesetStatus.query()\
2275 statuses = ChangesetStatus.query()\
2282 .filter(ChangesetStatus.repo == self)\
2276 .filter(ChangesetStatus.repo == self)\
2283 .filter(ChangesetStatus.version == 0)
2277 .filter(ChangesetStatus.version == 0)
2284
2278
2285 if revisions:
2279 if revisions:
2286 # Try doing the filtering in chunks to avoid hitting limits
2280 # Try doing the filtering in chunks to avoid hitting limits
2287 size = 500
2281 size = 500
2288 status_results = []
2282 status_results = []
2289 for chunk in xrange(0, len(revisions), size):
2283 for chunk in xrange(0, len(revisions), size):
2290 status_results += statuses.filter(
2284 status_results += statuses.filter(
2291 ChangesetStatus.revision.in_(
2285 ChangesetStatus.revision.in_(
2292 revisions[chunk: chunk+size])
2286 revisions[chunk: chunk+size])
2293 ).all()
2287 ).all()
2294 else:
2288 else:
2295 status_results = statuses.all()
2289 status_results = statuses.all()
2296
2290
2297 grouped = {}
2291 grouped = {}
2298
2292
2299 # maybe we have open new pullrequest without a status?
2293 # maybe we have open new pullrequest without a status?
2300 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2294 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2301 status_lbl = ChangesetStatus.get_status_lbl(stat)
2295 status_lbl = ChangesetStatus.get_status_lbl(stat)
2302 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2296 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2303 for rev in pr.revisions:
2297 for rev in pr.revisions:
2304 pr_id = pr.pull_request_id
2298 pr_id = pr.pull_request_id
2305 pr_repo = pr.target_repo.repo_name
2299 pr_repo = pr.target_repo.repo_name
2306 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2300 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2307
2301
2308 for stat in status_results:
2302 for stat in status_results:
2309 pr_id = pr_repo = None
2303 pr_id = pr_repo = None
2310 if stat.pull_request:
2304 if stat.pull_request:
2311 pr_id = stat.pull_request.pull_request_id
2305 pr_id = stat.pull_request.pull_request_id
2312 pr_repo = stat.pull_request.target_repo.repo_name
2306 pr_repo = stat.pull_request.target_repo.repo_name
2313 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2307 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2314 pr_id, pr_repo]
2308 pr_id, pr_repo]
2315 return grouped
2309 return grouped
2316
2310
2317 # ==========================================================================
2311 # ==========================================================================
2318 # SCM CACHE INSTANCE
2312 # SCM CACHE INSTANCE
2319 # ==========================================================================
2313 # ==========================================================================
2320
2314
2321 def scm_instance(self, **kwargs):
2315 def scm_instance(self, **kwargs):
2322 import rhodecode
2316 import rhodecode
2323
2317
2324 # Passing a config will not hit the cache currently only used
2318 # Passing a config will not hit the cache currently only used
2325 # for repo2dbmapper
2319 # for repo2dbmapper
2326 config = kwargs.pop('config', None)
2320 config = kwargs.pop('config', None)
2327 cache = kwargs.pop('cache', None)
2321 cache = kwargs.pop('cache', None)
2328 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2322 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2329 # if cache is NOT defined use default global, else we have a full
2323 # if cache is NOT defined use default global, else we have a full
2330 # control over cache behaviour
2324 # control over cache behaviour
2331 if cache is None and full_cache and not config:
2325 if cache is None and full_cache and not config:
2332 return self._get_instance_cached()
2326 return self._get_instance_cached()
2333 return self._get_instance(cache=bool(cache), config=config)
2327 return self._get_instance(cache=bool(cache), config=config)
2334
2328
2335 def _get_instance_cached(self):
2329 def _get_instance_cached(self):
2336 @cache_region('long_term')
2330 @cache_region('long_term')
2337 def _get_repo(cache_key):
2331 def _get_repo(cache_key):
2338 return self._get_instance()
2332 return self._get_instance()
2339
2333
2340 invalidator_context = CacheKey.repo_context_cache(
2334 invalidator_context = CacheKey.repo_context_cache(
2341 _get_repo, self.repo_name, None, thread_scoped=True)
2335 _get_repo, self.repo_name, None, thread_scoped=True)
2342
2336
2343 with invalidator_context as context:
2337 with invalidator_context as context:
2344 context.invalidate()
2338 context.invalidate()
2345 repo = context.compute()
2339 repo = context.compute()
2346
2340
2347 return repo
2341 return repo
2348
2342
2349 def _get_instance(self, cache=True, config=None):
2343 def _get_instance(self, cache=True, config=None):
2350 config = config or self._config
2344 config = config or self._config
2351 custom_wire = {
2345 custom_wire = {
2352 'cache': cache # controls the vcs.remote cache
2346 'cache': cache # controls the vcs.remote cache
2353 }
2347 }
2354 repo = get_vcs_instance(
2348 repo = get_vcs_instance(
2355 repo_path=safe_str(self.repo_full_path),
2349 repo_path=safe_str(self.repo_full_path),
2356 config=config,
2350 config=config,
2357 with_wire=custom_wire,
2351 with_wire=custom_wire,
2358 create=False,
2352 create=False,
2359 _vcs_alias=self.repo_type)
2353 _vcs_alias=self.repo_type)
2360
2354
2361 return repo
2355 return repo
2362
2356
2363 def __json__(self):
2357 def __json__(self):
2364 return {'landing_rev': self.landing_rev}
2358 return {'landing_rev': self.landing_rev}
2365
2359
2366 def get_dict(self):
2360 def get_dict(self):
2367
2361
2368 # Since we transformed `repo_name` to a hybrid property, we need to
2362 # Since we transformed `repo_name` to a hybrid property, we need to
2369 # keep compatibility with the code which uses `repo_name` field.
2363 # keep compatibility with the code which uses `repo_name` field.
2370
2364
2371 result = super(Repository, self).get_dict()
2365 result = super(Repository, self).get_dict()
2372 result['repo_name'] = result.pop('_repo_name', None)
2366 result['repo_name'] = result.pop('_repo_name', None)
2373 return result
2367 return result
2374
2368
2375
2369
2376 class RepoGroup(Base, BaseModel):
2370 class RepoGroup(Base, BaseModel):
2377 __tablename__ = 'groups'
2371 __tablename__ = 'groups'
2378 __table_args__ = (
2372 __table_args__ = (
2379 UniqueConstraint('group_name', 'group_parent_id'),
2373 UniqueConstraint('group_name', 'group_parent_id'),
2380 CheckConstraint('group_id != group_parent_id'),
2374 CheckConstraint('group_id != group_parent_id'),
2381 base_table_args,
2375 base_table_args,
2382 )
2376 )
2383 __mapper_args__ = {'order_by': 'group_name'}
2377 __mapper_args__ = {'order_by': 'group_name'}
2384
2378
2385 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2379 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2386
2380
2387 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2381 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2388 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2382 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2389 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2383 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2390 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2384 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2391 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2385 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2392 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2386 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2393 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2387 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2394 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2388 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2395 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2389 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2396
2390
2397 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2391 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2398 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2392 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2399 parent_group = relationship('RepoGroup', remote_side=group_id)
2393 parent_group = relationship('RepoGroup', remote_side=group_id)
2400 user = relationship('User')
2394 user = relationship('User')
2401 integrations = relationship('Integration',
2395 integrations = relationship('Integration',
2402 cascade="all, delete, delete-orphan")
2396 cascade="all, delete, delete-orphan")
2403
2397
2404 def __init__(self, group_name='', parent_group=None):
2398 def __init__(self, group_name='', parent_group=None):
2405 self.group_name = group_name
2399 self.group_name = group_name
2406 self.parent_group = parent_group
2400 self.parent_group = parent_group
2407
2401
2408 def __unicode__(self):
2402 def __unicode__(self):
2409 return u"<%s('id:%s:%s')>" % (
2403 return u"<%s('id:%s:%s')>" % (
2410 self.__class__.__name__, self.group_id, self.group_name)
2404 self.__class__.__name__, self.group_id, self.group_name)
2411
2405
2412 @hybrid_property
2406 @hybrid_property
2413 def description_safe(self):
2407 def description_safe(self):
2414 from rhodecode.lib import helpers as h
2408 from rhodecode.lib import helpers as h
2415 return h.escape(self.group_description)
2409 return h.escape(self.group_description)
2416
2410
2417 @classmethod
2411 @classmethod
2418 def _generate_choice(cls, repo_group):
2412 def _generate_choice(cls, repo_group):
2419 from webhelpers.html import literal as _literal
2413 from webhelpers.html import literal as _literal
2420 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2414 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2421 return repo_group.group_id, _name(repo_group.full_path_splitted)
2415 return repo_group.group_id, _name(repo_group.full_path_splitted)
2422
2416
2423 @classmethod
2417 @classmethod
2424 def groups_choices(cls, groups=None, show_empty_group=True):
2418 def groups_choices(cls, groups=None, show_empty_group=True):
2425 if not groups:
2419 if not groups:
2426 groups = cls.query().all()
2420 groups = cls.query().all()
2427
2421
2428 repo_groups = []
2422 repo_groups = []
2429 if show_empty_group:
2423 if show_empty_group:
2430 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2424 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2431
2425
2432 repo_groups.extend([cls._generate_choice(x) for x in groups])
2426 repo_groups.extend([cls._generate_choice(x) for x in groups])
2433
2427
2434 repo_groups = sorted(
2428 repo_groups = sorted(
2435 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2429 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2436 return repo_groups
2430 return repo_groups
2437
2431
2438 @classmethod
2432 @classmethod
2439 def url_sep(cls):
2433 def url_sep(cls):
2440 return URL_SEP
2434 return URL_SEP
2441
2435
2442 @classmethod
2436 @classmethod
2443 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2437 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2444 if case_insensitive:
2438 if case_insensitive:
2445 gr = cls.query().filter(func.lower(cls.group_name)
2439 gr = cls.query().filter(func.lower(cls.group_name)
2446 == func.lower(group_name))
2440 == func.lower(group_name))
2447 else:
2441 else:
2448 gr = cls.query().filter(cls.group_name == group_name)
2442 gr = cls.query().filter(cls.group_name == group_name)
2449 if cache:
2443 if cache:
2450 name_key = _hash_key(group_name)
2444 name_key = _hash_key(group_name)
2451 gr = gr.options(
2445 gr = gr.options(
2452 FromCache("sql_cache_short", "get_group_%s" % name_key))
2446 FromCache("sql_cache_short", "get_group_%s" % name_key))
2453 return gr.scalar()
2447 return gr.scalar()
2454
2448
2455 @classmethod
2449 @classmethod
2456 def get_user_personal_repo_group(cls, user_id):
2450 def get_user_personal_repo_group(cls, user_id):
2457 user = User.get(user_id)
2451 user = User.get(user_id)
2458 if user.username == User.DEFAULT_USER:
2452 if user.username == User.DEFAULT_USER:
2459 return None
2453 return None
2460
2454
2461 return cls.query()\
2455 return cls.query()\
2462 .filter(cls.personal == true()) \
2456 .filter(cls.personal == true()) \
2463 .filter(cls.user == user).scalar()
2457 .filter(cls.user == user).scalar()
2464
2458
2465 @classmethod
2459 @classmethod
2466 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2460 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2467 case_insensitive=True):
2461 case_insensitive=True):
2468 q = RepoGroup.query()
2462 q = RepoGroup.query()
2469
2463
2470 if not isinstance(user_id, Optional):
2464 if not isinstance(user_id, Optional):
2471 q = q.filter(RepoGroup.user_id == user_id)
2465 q = q.filter(RepoGroup.user_id == user_id)
2472
2466
2473 if not isinstance(group_id, Optional):
2467 if not isinstance(group_id, Optional):
2474 q = q.filter(RepoGroup.group_parent_id == group_id)
2468 q = q.filter(RepoGroup.group_parent_id == group_id)
2475
2469
2476 if case_insensitive:
2470 if case_insensitive:
2477 q = q.order_by(func.lower(RepoGroup.group_name))
2471 q = q.order_by(func.lower(RepoGroup.group_name))
2478 else:
2472 else:
2479 q = q.order_by(RepoGroup.group_name)
2473 q = q.order_by(RepoGroup.group_name)
2480 return q.all()
2474 return q.all()
2481
2475
2482 @property
2476 @property
2483 def parents(self):
2477 def parents(self):
2484 parents_recursion_limit = 10
2478 parents_recursion_limit = 10
2485 groups = []
2479 groups = []
2486 if self.parent_group is None:
2480 if self.parent_group is None:
2487 return groups
2481 return groups
2488 cur_gr = self.parent_group
2482 cur_gr = self.parent_group
2489 groups.insert(0, cur_gr)
2483 groups.insert(0, cur_gr)
2490 cnt = 0
2484 cnt = 0
2491 while 1:
2485 while 1:
2492 cnt += 1
2486 cnt += 1
2493 gr = getattr(cur_gr, 'parent_group', None)
2487 gr = getattr(cur_gr, 'parent_group', None)
2494 cur_gr = cur_gr.parent_group
2488 cur_gr = cur_gr.parent_group
2495 if gr is None:
2489 if gr is None:
2496 break
2490 break
2497 if cnt == parents_recursion_limit:
2491 if cnt == parents_recursion_limit:
2498 # this will prevent accidental infinit loops
2492 # this will prevent accidental infinit loops
2499 log.error(('more than %s parents found for group %s, stopping '
2493 log.error(('more than %s parents found for group %s, stopping '
2500 'recursive parent fetching' % (parents_recursion_limit, self)))
2494 'recursive parent fetching' % (parents_recursion_limit, self)))
2501 break
2495 break
2502
2496
2503 groups.insert(0, gr)
2497 groups.insert(0, gr)
2504 return groups
2498 return groups
2505
2499
2506 @property
2500 @property
2507 def last_db_change(self):
2501 def last_db_change(self):
2508 return self.updated_on
2502 return self.updated_on
2509
2503
2510 @property
2504 @property
2511 def children(self):
2505 def children(self):
2512 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2506 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2513
2507
2514 @property
2508 @property
2515 def name(self):
2509 def name(self):
2516 return self.group_name.split(RepoGroup.url_sep())[-1]
2510 return self.group_name.split(RepoGroup.url_sep())[-1]
2517
2511
2518 @property
2512 @property
2519 def full_path(self):
2513 def full_path(self):
2520 return self.group_name
2514 return self.group_name
2521
2515
2522 @property
2516 @property
2523 def full_path_splitted(self):
2517 def full_path_splitted(self):
2524 return self.group_name.split(RepoGroup.url_sep())
2518 return self.group_name.split(RepoGroup.url_sep())
2525
2519
2526 @property
2520 @property
2527 def repositories(self):
2521 def repositories(self):
2528 return Repository.query()\
2522 return Repository.query()\
2529 .filter(Repository.group == self)\
2523 .filter(Repository.group == self)\
2530 .order_by(Repository.repo_name)
2524 .order_by(Repository.repo_name)
2531
2525
2532 @property
2526 @property
2533 def repositories_recursive_count(self):
2527 def repositories_recursive_count(self):
2534 cnt = self.repositories.count()
2528 cnt = self.repositories.count()
2535
2529
2536 def children_count(group):
2530 def children_count(group):
2537 cnt = 0
2531 cnt = 0
2538 for child in group.children:
2532 for child in group.children:
2539 cnt += child.repositories.count()
2533 cnt += child.repositories.count()
2540 cnt += children_count(child)
2534 cnt += children_count(child)
2541 return cnt
2535 return cnt
2542
2536
2543 return cnt + children_count(self)
2537 return cnt + children_count(self)
2544
2538
2545 def _recursive_objects(self, include_repos=True):
2539 def _recursive_objects(self, include_repos=True):
2546 all_ = []
2540 all_ = []
2547
2541
2548 def _get_members(root_gr):
2542 def _get_members(root_gr):
2549 if include_repos:
2543 if include_repos:
2550 for r in root_gr.repositories:
2544 for r in root_gr.repositories:
2551 all_.append(r)
2545 all_.append(r)
2552 childs = root_gr.children.all()
2546 childs = root_gr.children.all()
2553 if childs:
2547 if childs:
2554 for gr in childs:
2548 for gr in childs:
2555 all_.append(gr)
2549 all_.append(gr)
2556 _get_members(gr)
2550 _get_members(gr)
2557
2551
2558 _get_members(self)
2552 _get_members(self)
2559 return [self] + all_
2553 return [self] + all_
2560
2554
2561 def recursive_groups_and_repos(self):
2555 def recursive_groups_and_repos(self):
2562 """
2556 """
2563 Recursive return all groups, with repositories in those groups
2557 Recursive return all groups, with repositories in those groups
2564 """
2558 """
2565 return self._recursive_objects()
2559 return self._recursive_objects()
2566
2560
2567 def recursive_groups(self):
2561 def recursive_groups(self):
2568 """
2562 """
2569 Returns all children groups for this group including children of children
2563 Returns all children groups for this group including children of children
2570 """
2564 """
2571 return self._recursive_objects(include_repos=False)
2565 return self._recursive_objects(include_repos=False)
2572
2566
2573 def get_new_name(self, group_name):
2567 def get_new_name(self, group_name):
2574 """
2568 """
2575 returns new full group name based on parent and new name
2569 returns new full group name based on parent and new name
2576
2570
2577 :param group_name:
2571 :param group_name:
2578 """
2572 """
2579 path_prefix = (self.parent_group.full_path_splitted if
2573 path_prefix = (self.parent_group.full_path_splitted if
2580 self.parent_group else [])
2574 self.parent_group else [])
2581 return RepoGroup.url_sep().join(path_prefix + [group_name])
2575 return RepoGroup.url_sep().join(path_prefix + [group_name])
2582
2576
2583 def permissions(self, with_admins=True, with_owner=True):
2577 def permissions(self, with_admins=True, with_owner=True):
2584 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2578 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2585 q = q.options(joinedload(UserRepoGroupToPerm.group),
2579 q = q.options(joinedload(UserRepoGroupToPerm.group),
2586 joinedload(UserRepoGroupToPerm.user),
2580 joinedload(UserRepoGroupToPerm.user),
2587 joinedload(UserRepoGroupToPerm.permission),)
2581 joinedload(UserRepoGroupToPerm.permission),)
2588
2582
2589 # get owners and admins and permissions. We do a trick of re-writing
2583 # get owners and admins and permissions. We do a trick of re-writing
2590 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2584 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2591 # has a global reference and changing one object propagates to all
2585 # has a global reference and changing one object propagates to all
2592 # others. This means if admin is also an owner admin_row that change
2586 # others. This means if admin is also an owner admin_row that change
2593 # would propagate to both objects
2587 # would propagate to both objects
2594 perm_rows = []
2588 perm_rows = []
2595 for _usr in q.all():
2589 for _usr in q.all():
2596 usr = AttributeDict(_usr.user.get_dict())
2590 usr = AttributeDict(_usr.user.get_dict())
2597 usr.permission = _usr.permission.permission_name
2591 usr.permission = _usr.permission.permission_name
2598 perm_rows.append(usr)
2592 perm_rows.append(usr)
2599
2593
2600 # filter the perm rows by 'default' first and then sort them by
2594 # filter the perm rows by 'default' first and then sort them by
2601 # admin,write,read,none permissions sorted again alphabetically in
2595 # admin,write,read,none permissions sorted again alphabetically in
2602 # each group
2596 # each group
2603 perm_rows = sorted(perm_rows, key=display_user_sort)
2597 perm_rows = sorted(perm_rows, key=display_user_sort)
2604
2598
2605 _admin_perm = 'group.admin'
2599 _admin_perm = 'group.admin'
2606 owner_row = []
2600 owner_row = []
2607 if with_owner:
2601 if with_owner:
2608 usr = AttributeDict(self.user.get_dict())
2602 usr = AttributeDict(self.user.get_dict())
2609 usr.owner_row = True
2603 usr.owner_row = True
2610 usr.permission = _admin_perm
2604 usr.permission = _admin_perm
2611 owner_row.append(usr)
2605 owner_row.append(usr)
2612
2606
2613 super_admin_rows = []
2607 super_admin_rows = []
2614 if with_admins:
2608 if with_admins:
2615 for usr in User.get_all_super_admins():
2609 for usr in User.get_all_super_admins():
2616 # if this admin is also owner, don't double the record
2610 # if this admin is also owner, don't double the record
2617 if usr.user_id == owner_row[0].user_id:
2611 if usr.user_id == owner_row[0].user_id:
2618 owner_row[0].admin_row = True
2612 owner_row[0].admin_row = True
2619 else:
2613 else:
2620 usr = AttributeDict(usr.get_dict())
2614 usr = AttributeDict(usr.get_dict())
2621 usr.admin_row = True
2615 usr.admin_row = True
2622 usr.permission = _admin_perm
2616 usr.permission = _admin_perm
2623 super_admin_rows.append(usr)
2617 super_admin_rows.append(usr)
2624
2618
2625 return super_admin_rows + owner_row + perm_rows
2619 return super_admin_rows + owner_row + perm_rows
2626
2620
2627 def permission_user_groups(self):
2621 def permission_user_groups(self):
2628 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2622 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2629 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2623 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2630 joinedload(UserGroupRepoGroupToPerm.users_group),
2624 joinedload(UserGroupRepoGroupToPerm.users_group),
2631 joinedload(UserGroupRepoGroupToPerm.permission),)
2625 joinedload(UserGroupRepoGroupToPerm.permission),)
2632
2626
2633 perm_rows = []
2627 perm_rows = []
2634 for _user_group in q.all():
2628 for _user_group in q.all():
2635 usr = AttributeDict(_user_group.users_group.get_dict())
2629 usr = AttributeDict(_user_group.users_group.get_dict())
2636 usr.permission = _user_group.permission.permission_name
2630 usr.permission = _user_group.permission.permission_name
2637 perm_rows.append(usr)
2631 perm_rows.append(usr)
2638
2632
2639 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2633 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2640 return perm_rows
2634 return perm_rows
2641
2635
2642 def get_api_data(self):
2636 def get_api_data(self):
2643 """
2637 """
2644 Common function for generating api data
2638 Common function for generating api data
2645
2639
2646 """
2640 """
2647 group = self
2641 group = self
2648 data = {
2642 data = {
2649 'group_id': group.group_id,
2643 'group_id': group.group_id,
2650 'group_name': group.group_name,
2644 'group_name': group.group_name,
2651 'group_description': group.description_safe,
2645 'group_description': group.description_safe,
2652 'parent_group': group.parent_group.group_name if group.parent_group else None,
2646 'parent_group': group.parent_group.group_name if group.parent_group else None,
2653 'repositories': [x.repo_name for x in group.repositories],
2647 'repositories': [x.repo_name for x in group.repositories],
2654 'owner': group.user.username,
2648 'owner': group.user.username,
2655 }
2649 }
2656 return data
2650 return data
2657
2651
2658
2652
2659 class Permission(Base, BaseModel):
2653 class Permission(Base, BaseModel):
2660 __tablename__ = 'permissions'
2654 __tablename__ = 'permissions'
2661 __table_args__ = (
2655 __table_args__ = (
2662 Index('p_perm_name_idx', 'permission_name'),
2656 Index('p_perm_name_idx', 'permission_name'),
2663 base_table_args,
2657 base_table_args,
2664 )
2658 )
2665
2659
2666 PERMS = [
2660 PERMS = [
2667 ('hg.admin', _('RhodeCode Super Administrator')),
2661 ('hg.admin', _('RhodeCode Super Administrator')),
2668
2662
2669 ('repository.none', _('Repository no access')),
2663 ('repository.none', _('Repository no access')),
2670 ('repository.read', _('Repository read access')),
2664 ('repository.read', _('Repository read access')),
2671 ('repository.write', _('Repository write access')),
2665 ('repository.write', _('Repository write access')),
2672 ('repository.admin', _('Repository admin access')),
2666 ('repository.admin', _('Repository admin access')),
2673
2667
2674 ('group.none', _('Repository group no access')),
2668 ('group.none', _('Repository group no access')),
2675 ('group.read', _('Repository group read access')),
2669 ('group.read', _('Repository group read access')),
2676 ('group.write', _('Repository group write access')),
2670 ('group.write', _('Repository group write access')),
2677 ('group.admin', _('Repository group admin access')),
2671 ('group.admin', _('Repository group admin access')),
2678
2672
2679 ('usergroup.none', _('User group no access')),
2673 ('usergroup.none', _('User group no access')),
2680 ('usergroup.read', _('User group read access')),
2674 ('usergroup.read', _('User group read access')),
2681 ('usergroup.write', _('User group write access')),
2675 ('usergroup.write', _('User group write access')),
2682 ('usergroup.admin', _('User group admin access')),
2676 ('usergroup.admin', _('User group admin access')),
2683
2677
2684 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2678 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2685 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2679 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2686
2680
2687 ('hg.usergroup.create.false', _('User Group creation disabled')),
2681 ('hg.usergroup.create.false', _('User Group creation disabled')),
2688 ('hg.usergroup.create.true', _('User Group creation enabled')),
2682 ('hg.usergroup.create.true', _('User Group creation enabled')),
2689
2683
2690 ('hg.create.none', _('Repository creation disabled')),
2684 ('hg.create.none', _('Repository creation disabled')),
2691 ('hg.create.repository', _('Repository creation enabled')),
2685 ('hg.create.repository', _('Repository creation enabled')),
2692 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2686 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2693 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2687 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2694
2688
2695 ('hg.fork.none', _('Repository forking disabled')),
2689 ('hg.fork.none', _('Repository forking disabled')),
2696 ('hg.fork.repository', _('Repository forking enabled')),
2690 ('hg.fork.repository', _('Repository forking enabled')),
2697
2691
2698 ('hg.register.none', _('Registration disabled')),
2692 ('hg.register.none', _('Registration disabled')),
2699 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2693 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2700 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2694 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2701
2695
2702 ('hg.password_reset.enabled', _('Password reset enabled')),
2696 ('hg.password_reset.enabled', _('Password reset enabled')),
2703 ('hg.password_reset.hidden', _('Password reset hidden')),
2697 ('hg.password_reset.hidden', _('Password reset hidden')),
2704 ('hg.password_reset.disabled', _('Password reset disabled')),
2698 ('hg.password_reset.disabled', _('Password reset disabled')),
2705
2699
2706 ('hg.extern_activate.manual', _('Manual activation of external account')),
2700 ('hg.extern_activate.manual', _('Manual activation of external account')),
2707 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2701 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2708
2702
2709 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2703 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2710 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2704 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2711 ]
2705 ]
2712
2706
2713 # definition of system default permissions for DEFAULT user
2707 # definition of system default permissions for DEFAULT user
2714 DEFAULT_USER_PERMISSIONS = [
2708 DEFAULT_USER_PERMISSIONS = [
2715 'repository.read',
2709 'repository.read',
2716 'group.read',
2710 'group.read',
2717 'usergroup.read',
2711 'usergroup.read',
2718 'hg.create.repository',
2712 'hg.create.repository',
2719 'hg.repogroup.create.false',
2713 'hg.repogroup.create.false',
2720 'hg.usergroup.create.false',
2714 'hg.usergroup.create.false',
2721 'hg.create.write_on_repogroup.true',
2715 'hg.create.write_on_repogroup.true',
2722 'hg.fork.repository',
2716 'hg.fork.repository',
2723 'hg.register.manual_activate',
2717 'hg.register.manual_activate',
2724 'hg.password_reset.enabled',
2718 'hg.password_reset.enabled',
2725 'hg.extern_activate.auto',
2719 'hg.extern_activate.auto',
2726 'hg.inherit_default_perms.true',
2720 'hg.inherit_default_perms.true',
2727 ]
2721 ]
2728
2722
2729 # defines which permissions are more important higher the more important
2723 # defines which permissions are more important higher the more important
2730 # Weight defines which permissions are more important.
2724 # Weight defines which permissions are more important.
2731 # The higher number the more important.
2725 # The higher number the more important.
2732 PERM_WEIGHTS = {
2726 PERM_WEIGHTS = {
2733 'repository.none': 0,
2727 'repository.none': 0,
2734 'repository.read': 1,
2728 'repository.read': 1,
2735 'repository.write': 3,
2729 'repository.write': 3,
2736 'repository.admin': 4,
2730 'repository.admin': 4,
2737
2731
2738 'group.none': 0,
2732 'group.none': 0,
2739 'group.read': 1,
2733 'group.read': 1,
2740 'group.write': 3,
2734 'group.write': 3,
2741 'group.admin': 4,
2735 'group.admin': 4,
2742
2736
2743 'usergroup.none': 0,
2737 'usergroup.none': 0,
2744 'usergroup.read': 1,
2738 'usergroup.read': 1,
2745 'usergroup.write': 3,
2739 'usergroup.write': 3,
2746 'usergroup.admin': 4,
2740 'usergroup.admin': 4,
2747
2741
2748 'hg.repogroup.create.false': 0,
2742 'hg.repogroup.create.false': 0,
2749 'hg.repogroup.create.true': 1,
2743 'hg.repogroup.create.true': 1,
2750
2744
2751 'hg.usergroup.create.false': 0,
2745 'hg.usergroup.create.false': 0,
2752 'hg.usergroup.create.true': 1,
2746 'hg.usergroup.create.true': 1,
2753
2747
2754 'hg.fork.none': 0,
2748 'hg.fork.none': 0,
2755 'hg.fork.repository': 1,
2749 'hg.fork.repository': 1,
2756 'hg.create.none': 0,
2750 'hg.create.none': 0,
2757 'hg.create.repository': 1
2751 'hg.create.repository': 1
2758 }
2752 }
2759
2753
2760 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2754 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2755 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2762 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2756 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2763
2757
2764 def __unicode__(self):
2758 def __unicode__(self):
2765 return u"<%s('%s:%s')>" % (
2759 return u"<%s('%s:%s')>" % (
2766 self.__class__.__name__, self.permission_id, self.permission_name
2760 self.__class__.__name__, self.permission_id, self.permission_name
2767 )
2761 )
2768
2762
2769 @classmethod
2763 @classmethod
2770 def get_by_key(cls, key):
2764 def get_by_key(cls, key):
2771 return cls.query().filter(cls.permission_name == key).scalar()
2765 return cls.query().filter(cls.permission_name == key).scalar()
2772
2766
2773 @classmethod
2767 @classmethod
2774 def get_default_repo_perms(cls, user_id, repo_id=None):
2768 def get_default_repo_perms(cls, user_id, repo_id=None):
2775 q = Session().query(UserRepoToPerm, Repository, Permission)\
2769 q = Session().query(UserRepoToPerm, Repository, Permission)\
2776 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2770 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2777 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2771 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2778 .filter(UserRepoToPerm.user_id == user_id)
2772 .filter(UserRepoToPerm.user_id == user_id)
2779 if repo_id:
2773 if repo_id:
2780 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2774 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2781 return q.all()
2775 return q.all()
2782
2776
2783 @classmethod
2777 @classmethod
2784 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2778 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2785 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2779 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2786 .join(
2780 .join(
2787 Permission,
2781 Permission,
2788 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2782 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2789 .join(
2783 .join(
2790 Repository,
2784 Repository,
2791 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2785 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2792 .join(
2786 .join(
2793 UserGroup,
2787 UserGroup,
2794 UserGroupRepoToPerm.users_group_id ==
2788 UserGroupRepoToPerm.users_group_id ==
2795 UserGroup.users_group_id)\
2789 UserGroup.users_group_id)\
2796 .join(
2790 .join(
2797 UserGroupMember,
2791 UserGroupMember,
2798 UserGroupRepoToPerm.users_group_id ==
2792 UserGroupRepoToPerm.users_group_id ==
2799 UserGroupMember.users_group_id)\
2793 UserGroupMember.users_group_id)\
2800 .filter(
2794 .filter(
2801 UserGroupMember.user_id == user_id,
2795 UserGroupMember.user_id == user_id,
2802 UserGroup.users_group_active == true())
2796 UserGroup.users_group_active == true())
2803 if repo_id:
2797 if repo_id:
2804 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2798 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2805 return q.all()
2799 return q.all()
2806
2800
2807 @classmethod
2801 @classmethod
2808 def get_default_group_perms(cls, user_id, repo_group_id=None):
2802 def get_default_group_perms(cls, user_id, repo_group_id=None):
2809 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2803 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2810 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2804 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2811 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2805 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2812 .filter(UserRepoGroupToPerm.user_id == user_id)
2806 .filter(UserRepoGroupToPerm.user_id == user_id)
2813 if repo_group_id:
2807 if repo_group_id:
2814 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2808 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2815 return q.all()
2809 return q.all()
2816
2810
2817 @classmethod
2811 @classmethod
2818 def get_default_group_perms_from_user_group(
2812 def get_default_group_perms_from_user_group(
2819 cls, user_id, repo_group_id=None):
2813 cls, user_id, repo_group_id=None):
2820 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2814 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2821 .join(
2815 .join(
2822 Permission,
2816 Permission,
2823 UserGroupRepoGroupToPerm.permission_id ==
2817 UserGroupRepoGroupToPerm.permission_id ==
2824 Permission.permission_id)\
2818 Permission.permission_id)\
2825 .join(
2819 .join(
2826 RepoGroup,
2820 RepoGroup,
2827 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2821 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2828 .join(
2822 .join(
2829 UserGroup,
2823 UserGroup,
2830 UserGroupRepoGroupToPerm.users_group_id ==
2824 UserGroupRepoGroupToPerm.users_group_id ==
2831 UserGroup.users_group_id)\
2825 UserGroup.users_group_id)\
2832 .join(
2826 .join(
2833 UserGroupMember,
2827 UserGroupMember,
2834 UserGroupRepoGroupToPerm.users_group_id ==
2828 UserGroupRepoGroupToPerm.users_group_id ==
2835 UserGroupMember.users_group_id)\
2829 UserGroupMember.users_group_id)\
2836 .filter(
2830 .filter(
2837 UserGroupMember.user_id == user_id,
2831 UserGroupMember.user_id == user_id,
2838 UserGroup.users_group_active == true())
2832 UserGroup.users_group_active == true())
2839 if repo_group_id:
2833 if repo_group_id:
2840 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2834 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2841 return q.all()
2835 return q.all()
2842
2836
2843 @classmethod
2837 @classmethod
2844 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2838 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2845 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2839 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2846 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2840 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2847 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2841 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2848 .filter(UserUserGroupToPerm.user_id == user_id)
2842 .filter(UserUserGroupToPerm.user_id == user_id)
2849 if user_group_id:
2843 if user_group_id:
2850 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2844 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2851 return q.all()
2845 return q.all()
2852
2846
2853 @classmethod
2847 @classmethod
2854 def get_default_user_group_perms_from_user_group(
2848 def get_default_user_group_perms_from_user_group(
2855 cls, user_id, user_group_id=None):
2849 cls, user_id, user_group_id=None):
2856 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2850 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2857 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2851 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2858 .join(
2852 .join(
2859 Permission,
2853 Permission,
2860 UserGroupUserGroupToPerm.permission_id ==
2854 UserGroupUserGroupToPerm.permission_id ==
2861 Permission.permission_id)\
2855 Permission.permission_id)\
2862 .join(
2856 .join(
2863 TargetUserGroup,
2857 TargetUserGroup,
2864 UserGroupUserGroupToPerm.target_user_group_id ==
2858 UserGroupUserGroupToPerm.target_user_group_id ==
2865 TargetUserGroup.users_group_id)\
2859 TargetUserGroup.users_group_id)\
2866 .join(
2860 .join(
2867 UserGroup,
2861 UserGroup,
2868 UserGroupUserGroupToPerm.user_group_id ==
2862 UserGroupUserGroupToPerm.user_group_id ==
2869 UserGroup.users_group_id)\
2863 UserGroup.users_group_id)\
2870 .join(
2864 .join(
2871 UserGroupMember,
2865 UserGroupMember,
2872 UserGroupUserGroupToPerm.user_group_id ==
2866 UserGroupUserGroupToPerm.user_group_id ==
2873 UserGroupMember.users_group_id)\
2867 UserGroupMember.users_group_id)\
2874 .filter(
2868 .filter(
2875 UserGroupMember.user_id == user_id,
2869 UserGroupMember.user_id == user_id,
2876 UserGroup.users_group_active == true())
2870 UserGroup.users_group_active == true())
2877 if user_group_id:
2871 if user_group_id:
2878 q = q.filter(
2872 q = q.filter(
2879 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2873 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2880
2874
2881 return q.all()
2875 return q.all()
2882
2876
2883
2877
2884 class UserRepoToPerm(Base, BaseModel):
2878 class UserRepoToPerm(Base, BaseModel):
2885 __tablename__ = 'repo_to_perm'
2879 __tablename__ = 'repo_to_perm'
2886 __table_args__ = (
2880 __table_args__ = (
2887 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2881 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2888 base_table_args
2882 base_table_args
2889 )
2883 )
2890
2884
2891 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2885 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2892 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2886 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2893 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2887 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2894 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2888 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2895
2889
2896 user = relationship('User')
2890 user = relationship('User')
2897 repository = relationship('Repository')
2891 repository = relationship('Repository')
2898 permission = relationship('Permission')
2892 permission = relationship('Permission')
2899
2893
2900 @classmethod
2894 @classmethod
2901 def create(cls, user, repository, permission):
2895 def create(cls, user, repository, permission):
2902 n = cls()
2896 n = cls()
2903 n.user = user
2897 n.user = user
2904 n.repository = repository
2898 n.repository = repository
2905 n.permission = permission
2899 n.permission = permission
2906 Session().add(n)
2900 Session().add(n)
2907 return n
2901 return n
2908
2902
2909 def __unicode__(self):
2903 def __unicode__(self):
2910 return u'<%s => %s >' % (self.user, self.repository)
2904 return u'<%s => %s >' % (self.user, self.repository)
2911
2905
2912
2906
2913 class UserUserGroupToPerm(Base, BaseModel):
2907 class UserUserGroupToPerm(Base, BaseModel):
2914 __tablename__ = 'user_user_group_to_perm'
2908 __tablename__ = 'user_user_group_to_perm'
2915 __table_args__ = (
2909 __table_args__ = (
2916 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2910 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2917 base_table_args
2911 base_table_args
2918 )
2912 )
2919
2913
2920 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2914 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2921 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2915 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2922 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2923 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2917 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2924
2918
2925 user = relationship('User')
2919 user = relationship('User')
2926 user_group = relationship('UserGroup')
2920 user_group = relationship('UserGroup')
2927 permission = relationship('Permission')
2921 permission = relationship('Permission')
2928
2922
2929 @classmethod
2923 @classmethod
2930 def create(cls, user, user_group, permission):
2924 def create(cls, user, user_group, permission):
2931 n = cls()
2925 n = cls()
2932 n.user = user
2926 n.user = user
2933 n.user_group = user_group
2927 n.user_group = user_group
2934 n.permission = permission
2928 n.permission = permission
2935 Session().add(n)
2929 Session().add(n)
2936 return n
2930 return n
2937
2931
2938 def __unicode__(self):
2932 def __unicode__(self):
2939 return u'<%s => %s >' % (self.user, self.user_group)
2933 return u'<%s => %s >' % (self.user, self.user_group)
2940
2934
2941
2935
2942 class UserToPerm(Base, BaseModel):
2936 class UserToPerm(Base, BaseModel):
2943 __tablename__ = 'user_to_perm'
2937 __tablename__ = 'user_to_perm'
2944 __table_args__ = (
2938 __table_args__ = (
2945 UniqueConstraint('user_id', 'permission_id'),
2939 UniqueConstraint('user_id', 'permission_id'),
2946 base_table_args
2940 base_table_args
2947 )
2941 )
2948
2942
2949 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2943 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2950 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2944 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2951 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2945 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2952
2946
2953 user = relationship('User')
2947 user = relationship('User')
2954 permission = relationship('Permission', lazy='joined')
2948 permission = relationship('Permission', lazy='joined')
2955
2949
2956 def __unicode__(self):
2950 def __unicode__(self):
2957 return u'<%s => %s >' % (self.user, self.permission)
2951 return u'<%s => %s >' % (self.user, self.permission)
2958
2952
2959
2953
2960 class UserGroupRepoToPerm(Base, BaseModel):
2954 class UserGroupRepoToPerm(Base, BaseModel):
2961 __tablename__ = 'users_group_repo_to_perm'
2955 __tablename__ = 'users_group_repo_to_perm'
2962 __table_args__ = (
2956 __table_args__ = (
2963 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2957 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2964 base_table_args
2958 base_table_args
2965 )
2959 )
2966
2960
2967 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2961 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2968 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2962 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2969 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2963 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2970 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2964 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2971
2965
2972 users_group = relationship('UserGroup')
2966 users_group = relationship('UserGroup')
2973 permission = relationship('Permission')
2967 permission = relationship('Permission')
2974 repository = relationship('Repository')
2968 repository = relationship('Repository')
2975
2969
2976 @classmethod
2970 @classmethod
2977 def create(cls, users_group, repository, permission):
2971 def create(cls, users_group, repository, permission):
2978 n = cls()
2972 n = cls()
2979 n.users_group = users_group
2973 n.users_group = users_group
2980 n.repository = repository
2974 n.repository = repository
2981 n.permission = permission
2975 n.permission = permission
2982 Session().add(n)
2976 Session().add(n)
2983 return n
2977 return n
2984
2978
2985 def __unicode__(self):
2979 def __unicode__(self):
2986 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2980 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2987
2981
2988
2982
2989 class UserGroupUserGroupToPerm(Base, BaseModel):
2983 class UserGroupUserGroupToPerm(Base, BaseModel):
2990 __tablename__ = 'user_group_user_group_to_perm'
2984 __tablename__ = 'user_group_user_group_to_perm'
2991 __table_args__ = (
2985 __table_args__ = (
2992 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2986 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2993 CheckConstraint('target_user_group_id != user_group_id'),
2987 CheckConstraint('target_user_group_id != user_group_id'),
2994 base_table_args
2988 base_table_args
2995 )
2989 )
2996
2990
2997 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2991 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2998 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2992 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2999 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2993 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3000 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2994 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3001
2995
3002 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2996 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3003 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2997 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3004 permission = relationship('Permission')
2998 permission = relationship('Permission')
3005
2999
3006 @classmethod
3000 @classmethod
3007 def create(cls, target_user_group, user_group, permission):
3001 def create(cls, target_user_group, user_group, permission):
3008 n = cls()
3002 n = cls()
3009 n.target_user_group = target_user_group
3003 n.target_user_group = target_user_group
3010 n.user_group = user_group
3004 n.user_group = user_group
3011 n.permission = permission
3005 n.permission = permission
3012 Session().add(n)
3006 Session().add(n)
3013 return n
3007 return n
3014
3008
3015 def __unicode__(self):
3009 def __unicode__(self):
3016 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3010 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3017
3011
3018
3012
3019 class UserGroupToPerm(Base, BaseModel):
3013 class UserGroupToPerm(Base, BaseModel):
3020 __tablename__ = 'users_group_to_perm'
3014 __tablename__ = 'users_group_to_perm'
3021 __table_args__ = (
3015 __table_args__ = (
3022 UniqueConstraint('users_group_id', 'permission_id',),
3016 UniqueConstraint('users_group_id', 'permission_id',),
3023 base_table_args
3017 base_table_args
3024 )
3018 )
3025
3019
3026 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3020 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3027 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3021 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3028 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3022 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3029
3023
3030 users_group = relationship('UserGroup')
3024 users_group = relationship('UserGroup')
3031 permission = relationship('Permission')
3025 permission = relationship('Permission')
3032
3026
3033
3027
3034 class UserRepoGroupToPerm(Base, BaseModel):
3028 class UserRepoGroupToPerm(Base, BaseModel):
3035 __tablename__ = 'user_repo_group_to_perm'
3029 __tablename__ = 'user_repo_group_to_perm'
3036 __table_args__ = (
3030 __table_args__ = (
3037 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3031 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3038 base_table_args
3032 base_table_args
3039 )
3033 )
3040
3034
3041 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3035 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3042 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3036 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3043 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3037 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3044 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3038 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3045
3039
3046 user = relationship('User')
3040 user = relationship('User')
3047 group = relationship('RepoGroup')
3041 group = relationship('RepoGroup')
3048 permission = relationship('Permission')
3042 permission = relationship('Permission')
3049
3043
3050 @classmethod
3044 @classmethod
3051 def create(cls, user, repository_group, permission):
3045 def create(cls, user, repository_group, permission):
3052 n = cls()
3046 n = cls()
3053 n.user = user
3047 n.user = user
3054 n.group = repository_group
3048 n.group = repository_group
3055 n.permission = permission
3049 n.permission = permission
3056 Session().add(n)
3050 Session().add(n)
3057 return n
3051 return n
3058
3052
3059
3053
3060 class UserGroupRepoGroupToPerm(Base, BaseModel):
3054 class UserGroupRepoGroupToPerm(Base, BaseModel):
3061 __tablename__ = 'users_group_repo_group_to_perm'
3055 __tablename__ = 'users_group_repo_group_to_perm'
3062 __table_args__ = (
3056 __table_args__ = (
3063 UniqueConstraint('users_group_id', 'group_id'),
3057 UniqueConstraint('users_group_id', 'group_id'),
3064 base_table_args
3058 base_table_args
3065 )
3059 )
3066
3060
3067 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3061 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3068 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3062 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3069 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3063 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3070 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3064 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3071
3065
3072 users_group = relationship('UserGroup')
3066 users_group = relationship('UserGroup')
3073 permission = relationship('Permission')
3067 permission = relationship('Permission')
3074 group = relationship('RepoGroup')
3068 group = relationship('RepoGroup')
3075
3069
3076 @classmethod
3070 @classmethod
3077 def create(cls, user_group, repository_group, permission):
3071 def create(cls, user_group, repository_group, permission):
3078 n = cls()
3072 n = cls()
3079 n.users_group = user_group
3073 n.users_group = user_group
3080 n.group = repository_group
3074 n.group = repository_group
3081 n.permission = permission
3075 n.permission = permission
3082 Session().add(n)
3076 Session().add(n)
3083 return n
3077 return n
3084
3078
3085 def __unicode__(self):
3079 def __unicode__(self):
3086 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3080 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3087
3081
3088
3082
3089 class Statistics(Base, BaseModel):
3083 class Statistics(Base, BaseModel):
3090 __tablename__ = 'statistics'
3084 __tablename__ = 'statistics'
3091 __table_args__ = (
3085 __table_args__ = (
3092 base_table_args
3086 base_table_args
3093 )
3087 )
3094
3088
3095 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3089 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3096 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3090 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3097 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3091 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3098 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3092 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3099 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3093 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3100 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3094 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3101
3095
3102 repository = relationship('Repository', single_parent=True)
3096 repository = relationship('Repository', single_parent=True)
3103
3097
3104
3098
3105 class UserFollowing(Base, BaseModel):
3099 class UserFollowing(Base, BaseModel):
3106 __tablename__ = 'user_followings'
3100 __tablename__ = 'user_followings'
3107 __table_args__ = (
3101 __table_args__ = (
3108 UniqueConstraint('user_id', 'follows_repository_id'),
3102 UniqueConstraint('user_id', 'follows_repository_id'),
3109 UniqueConstraint('user_id', 'follows_user_id'),
3103 UniqueConstraint('user_id', 'follows_user_id'),
3110 base_table_args
3104 base_table_args
3111 )
3105 )
3112
3106
3113 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3107 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3108 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3115 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3109 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3116 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3110 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3117 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3111 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3118
3112
3119 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3113 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3120
3114
3121 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3115 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3122 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3116 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3123
3117
3124 @classmethod
3118 @classmethod
3125 def get_repo_followers(cls, repo_id):
3119 def get_repo_followers(cls, repo_id):
3126 return cls.query().filter(cls.follows_repo_id == repo_id)
3120 return cls.query().filter(cls.follows_repo_id == repo_id)
3127
3121
3128
3122
3129 class CacheKey(Base, BaseModel):
3123 class CacheKey(Base, BaseModel):
3130 __tablename__ = 'cache_invalidation'
3124 __tablename__ = 'cache_invalidation'
3131 __table_args__ = (
3125 __table_args__ = (
3132 UniqueConstraint('cache_key'),
3126 UniqueConstraint('cache_key'),
3133 Index('key_idx', 'cache_key'),
3127 Index('key_idx', 'cache_key'),
3134 base_table_args,
3128 base_table_args,
3135 )
3129 )
3136
3130
3137 CACHE_TYPE_ATOM = 'ATOM'
3131 CACHE_TYPE_ATOM = 'ATOM'
3138 CACHE_TYPE_RSS = 'RSS'
3132 CACHE_TYPE_RSS = 'RSS'
3139 CACHE_TYPE_README = 'README'
3133 CACHE_TYPE_README = 'README'
3140
3134
3141 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3135 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3142 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3136 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3143 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3137 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3144 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3138 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3145
3139
3146 def __init__(self, cache_key, cache_args=''):
3140 def __init__(self, cache_key, cache_args=''):
3147 self.cache_key = cache_key
3141 self.cache_key = cache_key
3148 self.cache_args = cache_args
3142 self.cache_args = cache_args
3149 self.cache_active = False
3143 self.cache_active = False
3150
3144
3151 def __unicode__(self):
3145 def __unicode__(self):
3152 return u"<%s('%s:%s[%s]')>" % (
3146 return u"<%s('%s:%s[%s]')>" % (
3153 self.__class__.__name__,
3147 self.__class__.__name__,
3154 self.cache_id, self.cache_key, self.cache_active)
3148 self.cache_id, self.cache_key, self.cache_active)
3155
3149
3156 def _cache_key_partition(self):
3150 def _cache_key_partition(self):
3157 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3151 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3158 return prefix, repo_name, suffix
3152 return prefix, repo_name, suffix
3159
3153
3160 def get_prefix(self):
3154 def get_prefix(self):
3161 """
3155 """
3162 Try to extract prefix from existing cache key. The key could consist
3156 Try to extract prefix from existing cache key. The key could consist
3163 of prefix, repo_name, suffix
3157 of prefix, repo_name, suffix
3164 """
3158 """
3165 # this returns prefix, repo_name, suffix
3159 # this returns prefix, repo_name, suffix
3166 return self._cache_key_partition()[0]
3160 return self._cache_key_partition()[0]
3167
3161
3168 def get_suffix(self):
3162 def get_suffix(self):
3169 """
3163 """
3170 get suffix that might have been used in _get_cache_key to
3164 get suffix that might have been used in _get_cache_key to
3171 generate self.cache_key. Only used for informational purposes
3165 generate self.cache_key. Only used for informational purposes
3172 in repo_edit.mako.
3166 in repo_edit.mako.
3173 """
3167 """
3174 # prefix, repo_name, suffix
3168 # prefix, repo_name, suffix
3175 return self._cache_key_partition()[2]
3169 return self._cache_key_partition()[2]
3176
3170
3177 @classmethod
3171 @classmethod
3178 def delete_all_cache(cls):
3172 def delete_all_cache(cls):
3179 """
3173 """
3180 Delete all cache keys from database.
3174 Delete all cache keys from database.
3181 Should only be run when all instances are down and all entries
3175 Should only be run when all instances are down and all entries
3182 thus stale.
3176 thus stale.
3183 """
3177 """
3184 cls.query().delete()
3178 cls.query().delete()
3185 Session().commit()
3179 Session().commit()
3186
3180
3187 @classmethod
3181 @classmethod
3188 def get_cache_key(cls, repo_name, cache_type):
3182 def get_cache_key(cls, repo_name, cache_type):
3189 """
3183 """
3190
3184
3191 Generate a cache key for this process of RhodeCode instance.
3185 Generate a cache key for this process of RhodeCode instance.
3192 Prefix most likely will be process id or maybe explicitly set
3186 Prefix most likely will be process id or maybe explicitly set
3193 instance_id from .ini file.
3187 instance_id from .ini file.
3194 """
3188 """
3195 import rhodecode
3189 import rhodecode
3196 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3190 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3197
3191
3198 repo_as_unicode = safe_unicode(repo_name)
3192 repo_as_unicode = safe_unicode(repo_name)
3199 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3193 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3200 if cache_type else repo_as_unicode
3194 if cache_type else repo_as_unicode
3201
3195
3202 return u'{}{}'.format(prefix, key)
3196 return u'{}{}'.format(prefix, key)
3203
3197
3204 @classmethod
3198 @classmethod
3205 def set_invalidate(cls, repo_name, delete=False):
3199 def set_invalidate(cls, repo_name, delete=False):
3206 """
3200 """
3207 Mark all caches of a repo as invalid in the database.
3201 Mark all caches of a repo as invalid in the database.
3208 """
3202 """
3209
3203
3210 try:
3204 try:
3211 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3205 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3212 if delete:
3206 if delete:
3213 log.debug('cache objects deleted for repo %s',
3207 log.debug('cache objects deleted for repo %s',
3214 safe_str(repo_name))
3208 safe_str(repo_name))
3215 qry.delete()
3209 qry.delete()
3216 else:
3210 else:
3217 log.debug('cache objects marked as invalid for repo %s',
3211 log.debug('cache objects marked as invalid for repo %s',
3218 safe_str(repo_name))
3212 safe_str(repo_name))
3219 qry.update({"cache_active": False})
3213 qry.update({"cache_active": False})
3220
3214
3221 Session().commit()
3215 Session().commit()
3222 except Exception:
3216 except Exception:
3223 log.exception(
3217 log.exception(
3224 'Cache key invalidation failed for repository %s',
3218 'Cache key invalidation failed for repository %s',
3225 safe_str(repo_name))
3219 safe_str(repo_name))
3226 Session().rollback()
3220 Session().rollback()
3227
3221
3228 @classmethod
3222 @classmethod
3229 def get_active_cache(cls, cache_key):
3223 def get_active_cache(cls, cache_key):
3230 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3224 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3231 if inv_obj:
3225 if inv_obj:
3232 return inv_obj
3226 return inv_obj
3233 return None
3227 return None
3234
3228
3235 @classmethod
3229 @classmethod
3236 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3230 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3237 thread_scoped=False):
3231 thread_scoped=False):
3238 """
3232 """
3239 @cache_region('long_term')
3233 @cache_region('long_term')
3240 def _heavy_calculation(cache_key):
3234 def _heavy_calculation(cache_key):
3241 return 'result'
3235 return 'result'
3242
3236
3243 cache_context = CacheKey.repo_context_cache(
3237 cache_context = CacheKey.repo_context_cache(
3244 _heavy_calculation, repo_name, cache_type)
3238 _heavy_calculation, repo_name, cache_type)
3245
3239
3246 with cache_context as context:
3240 with cache_context as context:
3247 context.invalidate()
3241 context.invalidate()
3248 computed = context.compute()
3242 computed = context.compute()
3249
3243
3250 assert computed == 'result'
3244 assert computed == 'result'
3251 """
3245 """
3252 from rhodecode.lib import caches
3246 from rhodecode.lib import caches
3253 return caches.InvalidationContext(
3247 return caches.InvalidationContext(
3254 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3248 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3255
3249
3256
3250
3257 class ChangesetComment(Base, BaseModel):
3251 class ChangesetComment(Base, BaseModel):
3258 __tablename__ = 'changeset_comments'
3252 __tablename__ = 'changeset_comments'
3259 __table_args__ = (
3253 __table_args__ = (
3260 Index('cc_revision_idx', 'revision'),
3254 Index('cc_revision_idx', 'revision'),
3261 base_table_args,
3255 base_table_args,
3262 )
3256 )
3263
3257
3264 COMMENT_OUTDATED = u'comment_outdated'
3258 COMMENT_OUTDATED = u'comment_outdated'
3265 COMMENT_TYPE_NOTE = u'note'
3259 COMMENT_TYPE_NOTE = u'note'
3266 COMMENT_TYPE_TODO = u'todo'
3260 COMMENT_TYPE_TODO = u'todo'
3267 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3261 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3268
3262
3269 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3263 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3270 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3264 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3271 revision = Column('revision', String(40), nullable=True)
3265 revision = Column('revision', String(40), nullable=True)
3272 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3266 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3273 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3267 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3274 line_no = Column('line_no', Unicode(10), nullable=True)
3268 line_no = Column('line_no', Unicode(10), nullable=True)
3275 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3269 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3276 f_path = Column('f_path', Unicode(1000), nullable=True)
3270 f_path = Column('f_path', Unicode(1000), nullable=True)
3277 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3271 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3278 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3272 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3279 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3273 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3280 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3274 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3281 renderer = Column('renderer', Unicode(64), nullable=True)
3275 renderer = Column('renderer', Unicode(64), nullable=True)
3282 display_state = Column('display_state', Unicode(128), nullable=True)
3276 display_state = Column('display_state', Unicode(128), nullable=True)
3283
3277
3284 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3278 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3285 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3279 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3286 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3280 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3287 author = relationship('User', lazy='joined')
3281 author = relationship('User', lazy='joined')
3288 repo = relationship('Repository')
3282 repo = relationship('Repository')
3289 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3283 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3290 pull_request = relationship('PullRequest', lazy='joined')
3284 pull_request = relationship('PullRequest', lazy='joined')
3291 pull_request_version = relationship('PullRequestVersion')
3285 pull_request_version = relationship('PullRequestVersion')
3292
3286
3293 @classmethod
3287 @classmethod
3294 def get_users(cls, revision=None, pull_request_id=None):
3288 def get_users(cls, revision=None, pull_request_id=None):
3295 """
3289 """
3296 Returns user associated with this ChangesetComment. ie those
3290 Returns user associated with this ChangesetComment. ie those
3297 who actually commented
3291 who actually commented
3298
3292
3299 :param cls:
3293 :param cls:
3300 :param revision:
3294 :param revision:
3301 """
3295 """
3302 q = Session().query(User)\
3296 q = Session().query(User)\
3303 .join(ChangesetComment.author)
3297 .join(ChangesetComment.author)
3304 if revision:
3298 if revision:
3305 q = q.filter(cls.revision == revision)
3299 q = q.filter(cls.revision == revision)
3306 elif pull_request_id:
3300 elif pull_request_id:
3307 q = q.filter(cls.pull_request_id == pull_request_id)
3301 q = q.filter(cls.pull_request_id == pull_request_id)
3308 return q.all()
3302 return q.all()
3309
3303
3310 @classmethod
3304 @classmethod
3311 def get_index_from_version(cls, pr_version, versions):
3305 def get_index_from_version(cls, pr_version, versions):
3312 num_versions = [x.pull_request_version_id for x in versions]
3306 num_versions = [x.pull_request_version_id for x in versions]
3313 try:
3307 try:
3314 return num_versions.index(pr_version) +1
3308 return num_versions.index(pr_version) +1
3315 except (IndexError, ValueError):
3309 except (IndexError, ValueError):
3316 return
3310 return
3317
3311
3318 @property
3312 @property
3319 def outdated(self):
3313 def outdated(self):
3320 return self.display_state == self.COMMENT_OUTDATED
3314 return self.display_state == self.COMMENT_OUTDATED
3321
3315
3322 def outdated_at_version(self, version):
3316 def outdated_at_version(self, version):
3323 """
3317 """
3324 Checks if comment is outdated for given pull request version
3318 Checks if comment is outdated for given pull request version
3325 """
3319 """
3326 return self.outdated and self.pull_request_version_id != version
3320 return self.outdated and self.pull_request_version_id != version
3327
3321
3328 def older_than_version(self, version):
3322 def older_than_version(self, version):
3329 """
3323 """
3330 Checks if comment is made from previous version than given
3324 Checks if comment is made from previous version than given
3331 """
3325 """
3332 if version is None:
3326 if version is None:
3333 return self.pull_request_version_id is not None
3327 return self.pull_request_version_id is not None
3334
3328
3335 return self.pull_request_version_id < version
3329 return self.pull_request_version_id < version
3336
3330
3337 @property
3331 @property
3338 def resolved(self):
3332 def resolved(self):
3339 return self.resolved_by[0] if self.resolved_by else None
3333 return self.resolved_by[0] if self.resolved_by else None
3340
3334
3341 @property
3335 @property
3342 def is_todo(self):
3336 def is_todo(self):
3343 return self.comment_type == self.COMMENT_TYPE_TODO
3337 return self.comment_type == self.COMMENT_TYPE_TODO
3344
3338
3345 @property
3339 @property
3346 def is_inline(self):
3340 def is_inline(self):
3347 return self.line_no and self.f_path
3341 return self.line_no and self.f_path
3348
3342
3349 def get_index_version(self, versions):
3343 def get_index_version(self, versions):
3350 return self.get_index_from_version(
3344 return self.get_index_from_version(
3351 self.pull_request_version_id, versions)
3345 self.pull_request_version_id, versions)
3352
3346
3353 def __repr__(self):
3347 def __repr__(self):
3354 if self.comment_id:
3348 if self.comment_id:
3355 return '<DB:Comment #%s>' % self.comment_id
3349 return '<DB:Comment #%s>' % self.comment_id
3356 else:
3350 else:
3357 return '<DB:Comment at %#x>' % id(self)
3351 return '<DB:Comment at %#x>' % id(self)
3358
3352
3359 def get_api_data(self):
3353 def get_api_data(self):
3360 comment = self
3354 comment = self
3361 data = {
3355 data = {
3362 'comment_id': comment.comment_id,
3356 'comment_id': comment.comment_id,
3363 'comment_type': comment.comment_type,
3357 'comment_type': comment.comment_type,
3364 'comment_text': comment.text,
3358 'comment_text': comment.text,
3365 'comment_status': comment.status_change,
3359 'comment_status': comment.status_change,
3366 'comment_f_path': comment.f_path,
3360 'comment_f_path': comment.f_path,
3367 'comment_lineno': comment.line_no,
3361 'comment_lineno': comment.line_no,
3368 'comment_author': comment.author,
3362 'comment_author': comment.author,
3369 'comment_created_on': comment.created_on
3363 'comment_created_on': comment.created_on
3370 }
3364 }
3371 return data
3365 return data
3372
3366
3373 def __json__(self):
3367 def __json__(self):
3374 data = dict()
3368 data = dict()
3375 data.update(self.get_api_data())
3369 data.update(self.get_api_data())
3376 return data
3370 return data
3377
3371
3378
3372
3379 class ChangesetStatus(Base, BaseModel):
3373 class ChangesetStatus(Base, BaseModel):
3380 __tablename__ = 'changeset_statuses'
3374 __tablename__ = 'changeset_statuses'
3381 __table_args__ = (
3375 __table_args__ = (
3382 Index('cs_revision_idx', 'revision'),
3376 Index('cs_revision_idx', 'revision'),
3383 Index('cs_version_idx', 'version'),
3377 Index('cs_version_idx', 'version'),
3384 UniqueConstraint('repo_id', 'revision', 'version'),
3378 UniqueConstraint('repo_id', 'revision', 'version'),
3385 base_table_args
3379 base_table_args
3386 )
3380 )
3387
3381
3388 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3382 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3389 STATUS_APPROVED = 'approved'
3383 STATUS_APPROVED = 'approved'
3390 STATUS_REJECTED = 'rejected'
3384 STATUS_REJECTED = 'rejected'
3391 STATUS_UNDER_REVIEW = 'under_review'
3385 STATUS_UNDER_REVIEW = 'under_review'
3392
3386
3393 STATUSES = [
3387 STATUSES = [
3394 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3388 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3395 (STATUS_APPROVED, _("Approved")),
3389 (STATUS_APPROVED, _("Approved")),
3396 (STATUS_REJECTED, _("Rejected")),
3390 (STATUS_REJECTED, _("Rejected")),
3397 (STATUS_UNDER_REVIEW, _("Under Review")),
3391 (STATUS_UNDER_REVIEW, _("Under Review")),
3398 ]
3392 ]
3399
3393
3400 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3394 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3401 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3395 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3402 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3396 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3403 revision = Column('revision', String(40), nullable=False)
3397 revision = Column('revision', String(40), nullable=False)
3404 status = Column('status', String(128), nullable=False, default=DEFAULT)
3398 status = Column('status', String(128), nullable=False, default=DEFAULT)
3405 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3399 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3406 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3400 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3407 version = Column('version', Integer(), nullable=False, default=0)
3401 version = Column('version', Integer(), nullable=False, default=0)
3408 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3402 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3409
3403
3410 author = relationship('User', lazy='joined')
3404 author = relationship('User', lazy='joined')
3411 repo = relationship('Repository')
3405 repo = relationship('Repository')
3412 comment = relationship('ChangesetComment', lazy='joined')
3406 comment = relationship('ChangesetComment', lazy='joined')
3413 pull_request = relationship('PullRequest', lazy='joined')
3407 pull_request = relationship('PullRequest', lazy='joined')
3414
3408
3415 def __unicode__(self):
3409 def __unicode__(self):
3416 return u"<%s('%s[v%s]:%s')>" % (
3410 return u"<%s('%s[v%s]:%s')>" % (
3417 self.__class__.__name__,
3411 self.__class__.__name__,
3418 self.status, self.version, self.author
3412 self.status, self.version, self.author
3419 )
3413 )
3420
3414
3421 @classmethod
3415 @classmethod
3422 def get_status_lbl(cls, value):
3416 def get_status_lbl(cls, value):
3423 return dict(cls.STATUSES).get(value)
3417 return dict(cls.STATUSES).get(value)
3424
3418
3425 @property
3419 @property
3426 def status_lbl(self):
3420 def status_lbl(self):
3427 return ChangesetStatus.get_status_lbl(self.status)
3421 return ChangesetStatus.get_status_lbl(self.status)
3428
3422
3429 def get_api_data(self):
3423 def get_api_data(self):
3430 status = self
3424 status = self
3431 data = {
3425 data = {
3432 'status_id': status.changeset_status_id,
3426 'status_id': status.changeset_status_id,
3433 'status': status.status,
3427 'status': status.status,
3434 }
3428 }
3435 return data
3429 return data
3436
3430
3437 def __json__(self):
3431 def __json__(self):
3438 data = dict()
3432 data = dict()
3439 data.update(self.get_api_data())
3433 data.update(self.get_api_data())
3440 return data
3434 return data
3441
3435
3442
3436
3443 class _PullRequestBase(BaseModel):
3437 class _PullRequestBase(BaseModel):
3444 """
3438 """
3445 Common attributes of pull request and version entries.
3439 Common attributes of pull request and version entries.
3446 """
3440 """
3447
3441
3448 # .status values
3442 # .status values
3449 STATUS_NEW = u'new'
3443 STATUS_NEW = u'new'
3450 STATUS_OPEN = u'open'
3444 STATUS_OPEN = u'open'
3451 STATUS_CLOSED = u'closed'
3445 STATUS_CLOSED = u'closed'
3452
3446
3453 title = Column('title', Unicode(255), nullable=True)
3447 title = Column('title', Unicode(255), nullable=True)
3454 description = Column(
3448 description = Column(
3455 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3449 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3456 nullable=True)
3450 nullable=True)
3457 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3451 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3458
3452
3459 # new/open/closed status of pull request (not approve/reject/etc)
3453 # new/open/closed status of pull request (not approve/reject/etc)
3460 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3454 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3461 created_on = Column(
3455 created_on = Column(
3462 'created_on', DateTime(timezone=False), nullable=False,
3456 'created_on', DateTime(timezone=False), nullable=False,
3463 default=datetime.datetime.now)
3457 default=datetime.datetime.now)
3464 updated_on = Column(
3458 updated_on = Column(
3465 'updated_on', DateTime(timezone=False), nullable=False,
3459 'updated_on', DateTime(timezone=False), nullable=False,
3466 default=datetime.datetime.now)
3460 default=datetime.datetime.now)
3467
3461
3468 @declared_attr
3462 @declared_attr
3469 def user_id(cls):
3463 def user_id(cls):
3470 return Column(
3464 return Column(
3471 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3465 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3472 unique=None)
3466 unique=None)
3473
3467
3474 # 500 revisions max
3468 # 500 revisions max
3475 _revisions = Column(
3469 _revisions = Column(
3476 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3470 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3477
3471
3478 @declared_attr
3472 @declared_attr
3479 def source_repo_id(cls):
3473 def source_repo_id(cls):
3480 # TODO: dan: rename column to source_repo_id
3474 # TODO: dan: rename column to source_repo_id
3481 return Column(
3475 return Column(
3482 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3476 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3483 nullable=False)
3477 nullable=False)
3484
3478
3485 source_ref = Column('org_ref', Unicode(255), nullable=False)
3479 source_ref = Column('org_ref', Unicode(255), nullable=False)
3486
3480
3487 @declared_attr
3481 @declared_attr
3488 def target_repo_id(cls):
3482 def target_repo_id(cls):
3489 # TODO: dan: rename column to target_repo_id
3483 # TODO: dan: rename column to target_repo_id
3490 return Column(
3484 return Column(
3491 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3485 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3492 nullable=False)
3486 nullable=False)
3493
3487
3494 target_ref = Column('other_ref', Unicode(255), nullable=False)
3488 target_ref = Column('other_ref', Unicode(255), nullable=False)
3495 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3489 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3496
3490
3497 # TODO: dan: rename column to last_merge_source_rev
3491 # TODO: dan: rename column to last_merge_source_rev
3498 _last_merge_source_rev = Column(
3492 _last_merge_source_rev = Column(
3499 'last_merge_org_rev', String(40), nullable=True)
3493 'last_merge_org_rev', String(40), nullable=True)
3500 # TODO: dan: rename column to last_merge_target_rev
3494 # TODO: dan: rename column to last_merge_target_rev
3501 _last_merge_target_rev = Column(
3495 _last_merge_target_rev = Column(
3502 'last_merge_other_rev', String(40), nullable=True)
3496 'last_merge_other_rev', String(40), nullable=True)
3503 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3497 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3504 merge_rev = Column('merge_rev', String(40), nullable=True)
3498 merge_rev = Column('merge_rev', String(40), nullable=True)
3505
3499
3506 reviewer_data = Column(
3500 reviewer_data = Column(
3507 'reviewer_data_json', MutationObj.as_mutable(
3501 'reviewer_data_json', MutationObj.as_mutable(
3508 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3502 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3509
3503
3510 @property
3504 @property
3511 def reviewer_data_json(self):
3505 def reviewer_data_json(self):
3512 return json.dumps(self.reviewer_data)
3506 return json.dumps(self.reviewer_data)
3513
3507
3514 @hybrid_property
3508 @hybrid_property
3515 def description_safe(self):
3509 def description_safe(self):
3516 from rhodecode.lib import helpers as h
3510 from rhodecode.lib import helpers as h
3517 return h.escape(self.description)
3511 return h.escape(self.description)
3518
3512
3519 @hybrid_property
3513 @hybrid_property
3520 def revisions(self):
3514 def revisions(self):
3521 return self._revisions.split(':') if self._revisions else []
3515 return self._revisions.split(':') if self._revisions else []
3522
3516
3523 @revisions.setter
3517 @revisions.setter
3524 def revisions(self, val):
3518 def revisions(self, val):
3525 self._revisions = ':'.join(val)
3519 self._revisions = ':'.join(val)
3526
3520
3527 @hybrid_property
3521 @hybrid_property
3528 def last_merge_status(self):
3522 def last_merge_status(self):
3529 return safe_int(self._last_merge_status)
3523 return safe_int(self._last_merge_status)
3530
3524
3531 @last_merge_status.setter
3525 @last_merge_status.setter
3532 def last_merge_status(self, val):
3526 def last_merge_status(self, val):
3533 self._last_merge_status = val
3527 self._last_merge_status = val
3534
3528
3535 @declared_attr
3529 @declared_attr
3536 def author(cls):
3530 def author(cls):
3537 return relationship('User', lazy='joined')
3531 return relationship('User', lazy='joined')
3538
3532
3539 @declared_attr
3533 @declared_attr
3540 def source_repo(cls):
3534 def source_repo(cls):
3541 return relationship(
3535 return relationship(
3542 'Repository',
3536 'Repository',
3543 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3537 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3544
3538
3545 @property
3539 @property
3546 def source_ref_parts(self):
3540 def source_ref_parts(self):
3547 return self.unicode_to_reference(self.source_ref)
3541 return self.unicode_to_reference(self.source_ref)
3548
3542
3549 @declared_attr
3543 @declared_attr
3550 def target_repo(cls):
3544 def target_repo(cls):
3551 return relationship(
3545 return relationship(
3552 'Repository',
3546 'Repository',
3553 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3547 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3554
3548
3555 @property
3549 @property
3556 def target_ref_parts(self):
3550 def target_ref_parts(self):
3557 return self.unicode_to_reference(self.target_ref)
3551 return self.unicode_to_reference(self.target_ref)
3558
3552
3559 @property
3553 @property
3560 def shadow_merge_ref(self):
3554 def shadow_merge_ref(self):
3561 return self.unicode_to_reference(self._shadow_merge_ref)
3555 return self.unicode_to_reference(self._shadow_merge_ref)
3562
3556
3563 @shadow_merge_ref.setter
3557 @shadow_merge_ref.setter
3564 def shadow_merge_ref(self, ref):
3558 def shadow_merge_ref(self, ref):
3565 self._shadow_merge_ref = self.reference_to_unicode(ref)
3559 self._shadow_merge_ref = self.reference_to_unicode(ref)
3566
3560
3567 def unicode_to_reference(self, raw):
3561 def unicode_to_reference(self, raw):
3568 """
3562 """
3569 Convert a unicode (or string) to a reference object.
3563 Convert a unicode (or string) to a reference object.
3570 If unicode evaluates to False it returns None.
3564 If unicode evaluates to False it returns None.
3571 """
3565 """
3572 if raw:
3566 if raw:
3573 refs = raw.split(':')
3567 refs = raw.split(':')
3574 return Reference(*refs)
3568 return Reference(*refs)
3575 else:
3569 else:
3576 return None
3570 return None
3577
3571
3578 def reference_to_unicode(self, ref):
3572 def reference_to_unicode(self, ref):
3579 """
3573 """
3580 Convert a reference object to unicode.
3574 Convert a reference object to unicode.
3581 If reference is None it returns None.
3575 If reference is None it returns None.
3582 """
3576 """
3583 if ref:
3577 if ref:
3584 return u':'.join(ref)
3578 return u':'.join(ref)
3585 else:
3579 else:
3586 return None
3580 return None
3587
3581
3588 def get_api_data(self, with_merge_state=True):
3582 def get_api_data(self, with_merge_state=True):
3589 from rhodecode.model.pull_request import PullRequestModel
3583 from rhodecode.model.pull_request import PullRequestModel
3590
3584
3591 pull_request = self
3585 pull_request = self
3592 if with_merge_state:
3586 if with_merge_state:
3593 merge_status = PullRequestModel().merge_status(pull_request)
3587 merge_status = PullRequestModel().merge_status(pull_request)
3594 merge_state = {
3588 merge_state = {
3595 'status': merge_status[0],
3589 'status': merge_status[0],
3596 'message': safe_unicode(merge_status[1]),
3590 'message': safe_unicode(merge_status[1]),
3597 }
3591 }
3598 else:
3592 else:
3599 merge_state = {'status': 'not_available',
3593 merge_state = {'status': 'not_available',
3600 'message': 'not_available'}
3594 'message': 'not_available'}
3601
3595
3602 merge_data = {
3596 merge_data = {
3603 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3597 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3604 'reference': (
3598 'reference': (
3605 pull_request.shadow_merge_ref._asdict()
3599 pull_request.shadow_merge_ref._asdict()
3606 if pull_request.shadow_merge_ref else None),
3600 if pull_request.shadow_merge_ref else None),
3607 }
3601 }
3608
3602
3609 data = {
3603 data = {
3610 'pull_request_id': pull_request.pull_request_id,
3604 'pull_request_id': pull_request.pull_request_id,
3611 'url': PullRequestModel().get_url(pull_request),
3605 'url': PullRequestModel().get_url(pull_request),
3612 'title': pull_request.title,
3606 'title': pull_request.title,
3613 'description': pull_request.description,
3607 'description': pull_request.description,
3614 'status': pull_request.status,
3608 'status': pull_request.status,
3615 'created_on': pull_request.created_on,
3609 'created_on': pull_request.created_on,
3616 'updated_on': pull_request.updated_on,
3610 'updated_on': pull_request.updated_on,
3617 'commit_ids': pull_request.revisions,
3611 'commit_ids': pull_request.revisions,
3618 'review_status': pull_request.calculated_review_status(),
3612 'review_status': pull_request.calculated_review_status(),
3619 'mergeable': merge_state,
3613 'mergeable': merge_state,
3620 'source': {
3614 'source': {
3621 'clone_url': pull_request.source_repo.clone_url(),
3615 'clone_url': pull_request.source_repo.clone_url(),
3622 'repository': pull_request.source_repo.repo_name,
3616 'repository': pull_request.source_repo.repo_name,
3623 'reference': {
3617 'reference': {
3624 'name': pull_request.source_ref_parts.name,
3618 'name': pull_request.source_ref_parts.name,
3625 'type': pull_request.source_ref_parts.type,
3619 'type': pull_request.source_ref_parts.type,
3626 'commit_id': pull_request.source_ref_parts.commit_id,
3620 'commit_id': pull_request.source_ref_parts.commit_id,
3627 },
3621 },
3628 },
3622 },
3629 'target': {
3623 'target': {
3630 'clone_url': pull_request.target_repo.clone_url(),
3624 'clone_url': pull_request.target_repo.clone_url(),
3631 'repository': pull_request.target_repo.repo_name,
3625 'repository': pull_request.target_repo.repo_name,
3632 'reference': {
3626 'reference': {
3633 'name': pull_request.target_ref_parts.name,
3627 'name': pull_request.target_ref_parts.name,
3634 'type': pull_request.target_ref_parts.type,
3628 'type': pull_request.target_ref_parts.type,
3635 'commit_id': pull_request.target_ref_parts.commit_id,
3629 'commit_id': pull_request.target_ref_parts.commit_id,
3636 },
3630 },
3637 },
3631 },
3638 'merge': merge_data,
3632 'merge': merge_data,
3639 'author': pull_request.author.get_api_data(include_secrets=False,
3633 'author': pull_request.author.get_api_data(include_secrets=False,
3640 details='basic'),
3634 details='basic'),
3641 'reviewers': [
3635 'reviewers': [
3642 {
3636 {
3643 'user': reviewer.get_api_data(include_secrets=False,
3637 'user': reviewer.get_api_data(include_secrets=False,
3644 details='basic'),
3638 details='basic'),
3645 'reasons': reasons,
3639 'reasons': reasons,
3646 'review_status': st[0][1].status if st else 'not_reviewed',
3640 'review_status': st[0][1].status if st else 'not_reviewed',
3647 }
3641 }
3648 for obj, reviewer, reasons, mandatory, st in
3642 for obj, reviewer, reasons, mandatory, st in
3649 pull_request.reviewers_statuses()
3643 pull_request.reviewers_statuses()
3650 ]
3644 ]
3651 }
3645 }
3652
3646
3653 return data
3647 return data
3654
3648
3655
3649
3656 class PullRequest(Base, _PullRequestBase):
3650 class PullRequest(Base, _PullRequestBase):
3657 __tablename__ = 'pull_requests'
3651 __tablename__ = 'pull_requests'
3658 __table_args__ = (
3652 __table_args__ = (
3659 base_table_args,
3653 base_table_args,
3660 )
3654 )
3661
3655
3662 pull_request_id = Column(
3656 pull_request_id = Column(
3663 'pull_request_id', Integer(), nullable=False, primary_key=True)
3657 'pull_request_id', Integer(), nullable=False, primary_key=True)
3664
3658
3665 def __repr__(self):
3659 def __repr__(self):
3666 if self.pull_request_id:
3660 if self.pull_request_id:
3667 return '<DB:PullRequest #%s>' % self.pull_request_id
3661 return '<DB:PullRequest #%s>' % self.pull_request_id
3668 else:
3662 else:
3669 return '<DB:PullRequest at %#x>' % id(self)
3663 return '<DB:PullRequest at %#x>' % id(self)
3670
3664
3671 reviewers = relationship('PullRequestReviewers',
3665 reviewers = relationship('PullRequestReviewers',
3672 cascade="all, delete, delete-orphan")
3666 cascade="all, delete, delete-orphan")
3673 statuses = relationship('ChangesetStatus',
3667 statuses = relationship('ChangesetStatus',
3674 cascade="all, delete, delete-orphan")
3668 cascade="all, delete, delete-orphan")
3675 comments = relationship('ChangesetComment',
3669 comments = relationship('ChangesetComment',
3676 cascade="all, delete, delete-orphan")
3670 cascade="all, delete, delete-orphan")
3677 versions = relationship('PullRequestVersion',
3671 versions = relationship('PullRequestVersion',
3678 cascade="all, delete, delete-orphan",
3672 cascade="all, delete, delete-orphan",
3679 lazy='dynamic')
3673 lazy='dynamic')
3680
3674
3681 @classmethod
3675 @classmethod
3682 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3676 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3683 internal_methods=None):
3677 internal_methods=None):
3684
3678
3685 class PullRequestDisplay(object):
3679 class PullRequestDisplay(object):
3686 """
3680 """
3687 Special object wrapper for showing PullRequest data via Versions
3681 Special object wrapper for showing PullRequest data via Versions
3688 It mimics PR object as close as possible. This is read only object
3682 It mimics PR object as close as possible. This is read only object
3689 just for display
3683 just for display
3690 """
3684 """
3691
3685
3692 def __init__(self, attrs, internal=None):
3686 def __init__(self, attrs, internal=None):
3693 self.attrs = attrs
3687 self.attrs = attrs
3694 # internal have priority over the given ones via attrs
3688 # internal have priority over the given ones via attrs
3695 self.internal = internal or ['versions']
3689 self.internal = internal or ['versions']
3696
3690
3697 def __getattr__(self, item):
3691 def __getattr__(self, item):
3698 if item in self.internal:
3692 if item in self.internal:
3699 return getattr(self, item)
3693 return getattr(self, item)
3700 try:
3694 try:
3701 return self.attrs[item]
3695 return self.attrs[item]
3702 except KeyError:
3696 except KeyError:
3703 raise AttributeError(
3697 raise AttributeError(
3704 '%s object has no attribute %s' % (self, item))
3698 '%s object has no attribute %s' % (self, item))
3705
3699
3706 def __repr__(self):
3700 def __repr__(self):
3707 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3701 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3708
3702
3709 def versions(self):
3703 def versions(self):
3710 return pull_request_obj.versions.order_by(
3704 return pull_request_obj.versions.order_by(
3711 PullRequestVersion.pull_request_version_id).all()
3705 PullRequestVersion.pull_request_version_id).all()
3712
3706
3713 def is_closed(self):
3707 def is_closed(self):
3714 return pull_request_obj.is_closed()
3708 return pull_request_obj.is_closed()
3715
3709
3716 @property
3710 @property
3717 def pull_request_version_id(self):
3711 def pull_request_version_id(self):
3718 return getattr(pull_request_obj, 'pull_request_version_id', None)
3712 return getattr(pull_request_obj, 'pull_request_version_id', None)
3719
3713
3720 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3714 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3721
3715
3722 attrs.author = StrictAttributeDict(
3716 attrs.author = StrictAttributeDict(
3723 pull_request_obj.author.get_api_data())
3717 pull_request_obj.author.get_api_data())
3724 if pull_request_obj.target_repo:
3718 if pull_request_obj.target_repo:
3725 attrs.target_repo = StrictAttributeDict(
3719 attrs.target_repo = StrictAttributeDict(
3726 pull_request_obj.target_repo.get_api_data())
3720 pull_request_obj.target_repo.get_api_data())
3727 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3721 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3728
3722
3729 if pull_request_obj.source_repo:
3723 if pull_request_obj.source_repo:
3730 attrs.source_repo = StrictAttributeDict(
3724 attrs.source_repo = StrictAttributeDict(
3731 pull_request_obj.source_repo.get_api_data())
3725 pull_request_obj.source_repo.get_api_data())
3732 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3726 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3733
3727
3734 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3728 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3735 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3729 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3736 attrs.revisions = pull_request_obj.revisions
3730 attrs.revisions = pull_request_obj.revisions
3737
3731
3738 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3732 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3739 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3733 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3740 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3734 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3741
3735
3742 return PullRequestDisplay(attrs, internal=internal_methods)
3736 return PullRequestDisplay(attrs, internal=internal_methods)
3743
3737
3744 def is_closed(self):
3738 def is_closed(self):
3745 return self.status == self.STATUS_CLOSED
3739 return self.status == self.STATUS_CLOSED
3746
3740
3747 def __json__(self):
3741 def __json__(self):
3748 return {
3742 return {
3749 'revisions': self.revisions,
3743 'revisions': self.revisions,
3750 }
3744 }
3751
3745
3752 def calculated_review_status(self):
3746 def calculated_review_status(self):
3753 from rhodecode.model.changeset_status import ChangesetStatusModel
3747 from rhodecode.model.changeset_status import ChangesetStatusModel
3754 return ChangesetStatusModel().calculated_review_status(self)
3748 return ChangesetStatusModel().calculated_review_status(self)
3755
3749
3756 def reviewers_statuses(self):
3750 def reviewers_statuses(self):
3757 from rhodecode.model.changeset_status import ChangesetStatusModel
3751 from rhodecode.model.changeset_status import ChangesetStatusModel
3758 return ChangesetStatusModel().reviewers_statuses(self)
3752 return ChangesetStatusModel().reviewers_statuses(self)
3759
3753
3760 @property
3754 @property
3761 def workspace_id(self):
3755 def workspace_id(self):
3762 from rhodecode.model.pull_request import PullRequestModel
3756 from rhodecode.model.pull_request import PullRequestModel
3763 return PullRequestModel()._workspace_id(self)
3757 return PullRequestModel()._workspace_id(self)
3764
3758
3765 def get_shadow_repo(self):
3759 def get_shadow_repo(self):
3766 workspace_id = self.workspace_id
3760 workspace_id = self.workspace_id
3767 vcs_obj = self.target_repo.scm_instance()
3761 vcs_obj = self.target_repo.scm_instance()
3768 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3762 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3769 self.target_repo.repo_id, workspace_id)
3763 self.target_repo.repo_id, workspace_id)
3770 if os.path.isdir(shadow_repository_path):
3764 if os.path.isdir(shadow_repository_path):
3771 return vcs_obj._get_shadow_instance(shadow_repository_path)
3765 return vcs_obj._get_shadow_instance(shadow_repository_path)
3772
3766
3773
3767
3774 class PullRequestVersion(Base, _PullRequestBase):
3768 class PullRequestVersion(Base, _PullRequestBase):
3775 __tablename__ = 'pull_request_versions'
3769 __tablename__ = 'pull_request_versions'
3776 __table_args__ = (
3770 __table_args__ = (
3777 base_table_args,
3771 base_table_args,
3778 )
3772 )
3779
3773
3780 pull_request_version_id = Column(
3774 pull_request_version_id = Column(
3781 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3775 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3782 pull_request_id = Column(
3776 pull_request_id = Column(
3783 'pull_request_id', Integer(),
3777 'pull_request_id', Integer(),
3784 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3778 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3785 pull_request = relationship('PullRequest')
3779 pull_request = relationship('PullRequest')
3786
3780
3787 def __repr__(self):
3781 def __repr__(self):
3788 if self.pull_request_version_id:
3782 if self.pull_request_version_id:
3789 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3783 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3790 else:
3784 else:
3791 return '<DB:PullRequestVersion at %#x>' % id(self)
3785 return '<DB:PullRequestVersion at %#x>' % id(self)
3792
3786
3793 @property
3787 @property
3794 def reviewers(self):
3788 def reviewers(self):
3795 return self.pull_request.reviewers
3789 return self.pull_request.reviewers
3796
3790
3797 @property
3791 @property
3798 def versions(self):
3792 def versions(self):
3799 return self.pull_request.versions
3793 return self.pull_request.versions
3800
3794
3801 def is_closed(self):
3795 def is_closed(self):
3802 # calculate from original
3796 # calculate from original
3803 return self.pull_request.status == self.STATUS_CLOSED
3797 return self.pull_request.status == self.STATUS_CLOSED
3804
3798
3805 def calculated_review_status(self):
3799 def calculated_review_status(self):
3806 return self.pull_request.calculated_review_status()
3800 return self.pull_request.calculated_review_status()
3807
3801
3808 def reviewers_statuses(self):
3802 def reviewers_statuses(self):
3809 return self.pull_request.reviewers_statuses()
3803 return self.pull_request.reviewers_statuses()
3810
3804
3811
3805
3812 class PullRequestReviewers(Base, BaseModel):
3806 class PullRequestReviewers(Base, BaseModel):
3813 __tablename__ = 'pull_request_reviewers'
3807 __tablename__ = 'pull_request_reviewers'
3814 __table_args__ = (
3808 __table_args__ = (
3815 base_table_args,
3809 base_table_args,
3816 )
3810 )
3817
3811
3818 @hybrid_property
3812 @hybrid_property
3819 def reasons(self):
3813 def reasons(self):
3820 if not self._reasons:
3814 if not self._reasons:
3821 return []
3815 return []
3822 return self._reasons
3816 return self._reasons
3823
3817
3824 @reasons.setter
3818 @reasons.setter
3825 def reasons(self, val):
3819 def reasons(self, val):
3826 val = val or []
3820 val = val or []
3827 if any(not isinstance(x, basestring) for x in val):
3821 if any(not isinstance(x, basestring) for x in val):
3828 raise Exception('invalid reasons type, must be list of strings')
3822 raise Exception('invalid reasons type, must be list of strings')
3829 self._reasons = val
3823 self._reasons = val
3830
3824
3831 pull_requests_reviewers_id = Column(
3825 pull_requests_reviewers_id = Column(
3832 'pull_requests_reviewers_id', Integer(), nullable=False,
3826 'pull_requests_reviewers_id', Integer(), nullable=False,
3833 primary_key=True)
3827 primary_key=True)
3834 pull_request_id = Column(
3828 pull_request_id = Column(
3835 "pull_request_id", Integer(),
3829 "pull_request_id", Integer(),
3836 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3830 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3837 user_id = Column(
3831 user_id = Column(
3838 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3832 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3839 _reasons = Column(
3833 _reasons = Column(
3840 'reason', MutationList.as_mutable(
3834 'reason', MutationList.as_mutable(
3841 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3835 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3842
3836
3843 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3837 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3844 user = relationship('User')
3838 user = relationship('User')
3845 pull_request = relationship('PullRequest')
3839 pull_request = relationship('PullRequest')
3846
3840
3847 rule_data = Column(
3841 rule_data = Column(
3848 'rule_data_json',
3842 'rule_data_json',
3849 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
3843 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
3850
3844
3851 def rule_user_group_data(self):
3845 def rule_user_group_data(self):
3852 """
3846 """
3853 Returns the voting user group rule data for this reviewer
3847 Returns the voting user group rule data for this reviewer
3854 """
3848 """
3855
3849
3856 if self.rule_data and 'vote_rule' in self.rule_data:
3850 if self.rule_data and 'vote_rule' in self.rule_data:
3857 user_group_data = {}
3851 user_group_data = {}
3858 if 'rule_user_group_entry_id' in self.rule_data:
3852 if 'rule_user_group_entry_id' in self.rule_data:
3859 # means a group with voting rules !
3853 # means a group with voting rules !
3860 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
3854 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
3861 user_group_data['name'] = self.rule_data['rule_name']
3855 user_group_data['name'] = self.rule_data['rule_name']
3862 user_group_data['vote_rule'] = self.rule_data['vote_rule']
3856 user_group_data['vote_rule'] = self.rule_data['vote_rule']
3863
3857
3864 return user_group_data
3858 return user_group_data
3865
3859
3866 def __unicode__(self):
3860 def __unicode__(self):
3867 return u"<%s('id:%s')>" % (self.__class__.__name__,
3861 return u"<%s('id:%s')>" % (self.__class__.__name__,
3868 self.pull_requests_reviewers_id)
3862 self.pull_requests_reviewers_id)
3869
3863
3870
3864
3871 class Notification(Base, BaseModel):
3865 class Notification(Base, BaseModel):
3872 __tablename__ = 'notifications'
3866 __tablename__ = 'notifications'
3873 __table_args__ = (
3867 __table_args__ = (
3874 Index('notification_type_idx', 'type'),
3868 Index('notification_type_idx', 'type'),
3875 base_table_args,
3869 base_table_args,
3876 )
3870 )
3877
3871
3878 TYPE_CHANGESET_COMMENT = u'cs_comment'
3872 TYPE_CHANGESET_COMMENT = u'cs_comment'
3879 TYPE_MESSAGE = u'message'
3873 TYPE_MESSAGE = u'message'
3880 TYPE_MENTION = u'mention'
3874 TYPE_MENTION = u'mention'
3881 TYPE_REGISTRATION = u'registration'
3875 TYPE_REGISTRATION = u'registration'
3882 TYPE_PULL_REQUEST = u'pull_request'
3876 TYPE_PULL_REQUEST = u'pull_request'
3883 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3877 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3884
3878
3885 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3879 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3886 subject = Column('subject', Unicode(512), nullable=True)
3880 subject = Column('subject', Unicode(512), nullable=True)
3887 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3881 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3888 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3882 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3889 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3883 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3890 type_ = Column('type', Unicode(255))
3884 type_ = Column('type', Unicode(255))
3891
3885
3892 created_by_user = relationship('User')
3886 created_by_user = relationship('User')
3893 notifications_to_users = relationship('UserNotification', lazy='joined',
3887 notifications_to_users = relationship('UserNotification', lazy='joined',
3894 cascade="all, delete, delete-orphan")
3888 cascade="all, delete, delete-orphan")
3895
3889
3896 @property
3890 @property
3897 def recipients(self):
3891 def recipients(self):
3898 return [x.user for x in UserNotification.query()\
3892 return [x.user for x in UserNotification.query()\
3899 .filter(UserNotification.notification == self)\
3893 .filter(UserNotification.notification == self)\
3900 .order_by(UserNotification.user_id.asc()).all()]
3894 .order_by(UserNotification.user_id.asc()).all()]
3901
3895
3902 @classmethod
3896 @classmethod
3903 def create(cls, created_by, subject, body, recipients, type_=None):
3897 def create(cls, created_by, subject, body, recipients, type_=None):
3904 if type_ is None:
3898 if type_ is None:
3905 type_ = Notification.TYPE_MESSAGE
3899 type_ = Notification.TYPE_MESSAGE
3906
3900
3907 notification = cls()
3901 notification = cls()
3908 notification.created_by_user = created_by
3902 notification.created_by_user = created_by
3909 notification.subject = subject
3903 notification.subject = subject
3910 notification.body = body
3904 notification.body = body
3911 notification.type_ = type_
3905 notification.type_ = type_
3912 notification.created_on = datetime.datetime.now()
3906 notification.created_on = datetime.datetime.now()
3913
3907
3914 # For each recipient link the created notification to his account
3908 # For each recipient link the created notification to his account
3915 for u in recipients:
3909 for u in recipients:
3916 assoc = UserNotification()
3910 assoc = UserNotification()
3917 assoc.user_id = u.user_id
3911 assoc.user_id = u.user_id
3918 assoc.notification = notification
3912 assoc.notification = notification
3919
3913
3920 # if created_by is inside recipients mark his notification
3914 # if created_by is inside recipients mark his notification
3921 # as read
3915 # as read
3922 if u.user_id == created_by.user_id:
3916 if u.user_id == created_by.user_id:
3923 assoc.read = True
3917 assoc.read = True
3924 Session().add(assoc)
3918 Session().add(assoc)
3925
3919
3926 Session().add(notification)
3920 Session().add(notification)
3927
3921
3928 return notification
3922 return notification
3929
3923
3930
3924
3931 class UserNotification(Base, BaseModel):
3925 class UserNotification(Base, BaseModel):
3932 __tablename__ = 'user_to_notification'
3926 __tablename__ = 'user_to_notification'
3933 __table_args__ = (
3927 __table_args__ = (
3934 UniqueConstraint('user_id', 'notification_id'),
3928 UniqueConstraint('user_id', 'notification_id'),
3935 base_table_args
3929 base_table_args
3936 )
3930 )
3937
3931
3938 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3932 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3939 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3933 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3940 read = Column('read', Boolean, default=False)
3934 read = Column('read', Boolean, default=False)
3941 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3935 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3942
3936
3943 user = relationship('User', lazy="joined")
3937 user = relationship('User', lazy="joined")
3944 notification = relationship('Notification', lazy="joined",
3938 notification = relationship('Notification', lazy="joined",
3945 order_by=lambda: Notification.created_on.desc(),)
3939 order_by=lambda: Notification.created_on.desc(),)
3946
3940
3947 def mark_as_read(self):
3941 def mark_as_read(self):
3948 self.read = True
3942 self.read = True
3949 Session().add(self)
3943 Session().add(self)
3950
3944
3951
3945
3952 class Gist(Base, BaseModel):
3946 class Gist(Base, BaseModel):
3953 __tablename__ = 'gists'
3947 __tablename__ = 'gists'
3954 __table_args__ = (
3948 __table_args__ = (
3955 Index('g_gist_access_id_idx', 'gist_access_id'),
3949 Index('g_gist_access_id_idx', 'gist_access_id'),
3956 Index('g_created_on_idx', 'created_on'),
3950 Index('g_created_on_idx', 'created_on'),
3957 base_table_args
3951 base_table_args
3958 )
3952 )
3959
3953
3960 GIST_PUBLIC = u'public'
3954 GIST_PUBLIC = u'public'
3961 GIST_PRIVATE = u'private'
3955 GIST_PRIVATE = u'private'
3962 DEFAULT_FILENAME = u'gistfile1.txt'
3956 DEFAULT_FILENAME = u'gistfile1.txt'
3963
3957
3964 ACL_LEVEL_PUBLIC = u'acl_public'
3958 ACL_LEVEL_PUBLIC = u'acl_public'
3965 ACL_LEVEL_PRIVATE = u'acl_private'
3959 ACL_LEVEL_PRIVATE = u'acl_private'
3966
3960
3967 gist_id = Column('gist_id', Integer(), primary_key=True)
3961 gist_id = Column('gist_id', Integer(), primary_key=True)
3968 gist_access_id = Column('gist_access_id', Unicode(250))
3962 gist_access_id = Column('gist_access_id', Unicode(250))
3969 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3963 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3970 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3964 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3971 gist_expires = Column('gist_expires', Float(53), nullable=False)
3965 gist_expires = Column('gist_expires', Float(53), nullable=False)
3972 gist_type = Column('gist_type', Unicode(128), nullable=False)
3966 gist_type = Column('gist_type', Unicode(128), nullable=False)
3973 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3967 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3974 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3968 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3975 acl_level = Column('acl_level', Unicode(128), nullable=True)
3969 acl_level = Column('acl_level', Unicode(128), nullable=True)
3976
3970
3977 owner = relationship('User')
3971 owner = relationship('User')
3978
3972
3979 def __repr__(self):
3973 def __repr__(self):
3980 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3974 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3981
3975
3982 @hybrid_property
3976 @hybrid_property
3983 def description_safe(self):
3977 def description_safe(self):
3984 from rhodecode.lib import helpers as h
3978 from rhodecode.lib import helpers as h
3985 return h.escape(self.gist_description)
3979 return h.escape(self.gist_description)
3986
3980
3987 @classmethod
3981 @classmethod
3988 def get_or_404(cls, id_):
3982 def get_or_404(cls, id_):
3989 from pyramid.httpexceptions import HTTPNotFound
3983 from pyramid.httpexceptions import HTTPNotFound
3990
3984
3991 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3985 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3992 if not res:
3986 if not res:
3993 raise HTTPNotFound()
3987 raise HTTPNotFound()
3994 return res
3988 return res
3995
3989
3996 @classmethod
3990 @classmethod
3997 def get_by_access_id(cls, gist_access_id):
3991 def get_by_access_id(cls, gist_access_id):
3998 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3992 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3999
3993
4000 def gist_url(self):
3994 def gist_url(self):
4001 from rhodecode.model.gist import GistModel
3995 from rhodecode.model.gist import GistModel
4002 return GistModel().get_url(self)
3996 return GistModel().get_url(self)
4003
3997
4004 @classmethod
3998 @classmethod
4005 def base_path(cls):
3999 def base_path(cls):
4006 """
4000 """
4007 Returns base path when all gists are stored
4001 Returns base path when all gists are stored
4008
4002
4009 :param cls:
4003 :param cls:
4010 """
4004 """
4011 from rhodecode.model.gist import GIST_STORE_LOC
4005 from rhodecode.model.gist import GIST_STORE_LOC
4012 q = Session().query(RhodeCodeUi)\
4006 q = Session().query(RhodeCodeUi)\
4013 .filter(RhodeCodeUi.ui_key == URL_SEP)
4007 .filter(RhodeCodeUi.ui_key == URL_SEP)
4014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4008 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4015 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4009 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4016
4010
4017 def get_api_data(self):
4011 def get_api_data(self):
4018 """
4012 """
4019 Common function for generating gist related data for API
4013 Common function for generating gist related data for API
4020 """
4014 """
4021 gist = self
4015 gist = self
4022 data = {
4016 data = {
4023 'gist_id': gist.gist_id,
4017 'gist_id': gist.gist_id,
4024 'type': gist.gist_type,
4018 'type': gist.gist_type,
4025 'access_id': gist.gist_access_id,
4019 'access_id': gist.gist_access_id,
4026 'description': gist.gist_description,
4020 'description': gist.gist_description,
4027 'url': gist.gist_url(),
4021 'url': gist.gist_url(),
4028 'expires': gist.gist_expires,
4022 'expires': gist.gist_expires,
4029 'created_on': gist.created_on,
4023 'created_on': gist.created_on,
4030 'modified_at': gist.modified_at,
4024 'modified_at': gist.modified_at,
4031 'content': None,
4025 'content': None,
4032 'acl_level': gist.acl_level,
4026 'acl_level': gist.acl_level,
4033 }
4027 }
4034 return data
4028 return data
4035
4029
4036 def __json__(self):
4030 def __json__(self):
4037 data = dict(
4031 data = dict(
4038 )
4032 )
4039 data.update(self.get_api_data())
4033 data.update(self.get_api_data())
4040 return data
4034 return data
4041 # SCM functions
4035 # SCM functions
4042
4036
4043 def scm_instance(self, **kwargs):
4037 def scm_instance(self, **kwargs):
4044 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4045 return get_vcs_instance(
4039 return get_vcs_instance(
4046 repo_path=safe_str(full_repo_path), create=False)
4040 repo_path=safe_str(full_repo_path), create=False)
4047
4041
4048
4042
4049 class ExternalIdentity(Base, BaseModel):
4043 class ExternalIdentity(Base, BaseModel):
4050 __tablename__ = 'external_identities'
4044 __tablename__ = 'external_identities'
4051 __table_args__ = (
4045 __table_args__ = (
4052 Index('local_user_id_idx', 'local_user_id'),
4046 Index('local_user_id_idx', 'local_user_id'),
4053 Index('external_id_idx', 'external_id'),
4047 Index('external_id_idx', 'external_id'),
4054 base_table_args
4048 base_table_args
4055 )
4049 )
4056
4050
4057 external_id = Column('external_id', Unicode(255), default=u'',
4051 external_id = Column('external_id', Unicode(255), default=u'',
4058 primary_key=True)
4052 primary_key=True)
4059 external_username = Column('external_username', Unicode(1024), default=u'')
4053 external_username = Column('external_username', Unicode(1024), default=u'')
4060 local_user_id = Column('local_user_id', Integer(),
4054 local_user_id = Column('local_user_id', Integer(),
4061 ForeignKey('users.user_id'), primary_key=True)
4055 ForeignKey('users.user_id'), primary_key=True)
4062 provider_name = Column('provider_name', Unicode(255), default=u'',
4056 provider_name = Column('provider_name', Unicode(255), default=u'',
4063 primary_key=True)
4057 primary_key=True)
4064 access_token = Column('access_token', String(1024), default=u'')
4058 access_token = Column('access_token', String(1024), default=u'')
4065 alt_token = Column('alt_token', String(1024), default=u'')
4059 alt_token = Column('alt_token', String(1024), default=u'')
4066 token_secret = Column('token_secret', String(1024), default=u'')
4060 token_secret = Column('token_secret', String(1024), default=u'')
4067
4061
4068 @classmethod
4062 @classmethod
4069 def by_external_id_and_provider(cls, external_id, provider_name,
4063 def by_external_id_and_provider(cls, external_id, provider_name,
4070 local_user_id=None):
4064 local_user_id=None):
4071 """
4065 """
4072 Returns ExternalIdentity instance based on search params
4066 Returns ExternalIdentity instance based on search params
4073
4067
4074 :param external_id:
4068 :param external_id:
4075 :param provider_name:
4069 :param provider_name:
4076 :return: ExternalIdentity
4070 :return: ExternalIdentity
4077 """
4071 """
4078 query = cls.query()
4072 query = cls.query()
4079 query = query.filter(cls.external_id == external_id)
4073 query = query.filter(cls.external_id == external_id)
4080 query = query.filter(cls.provider_name == provider_name)
4074 query = query.filter(cls.provider_name == provider_name)
4081 if local_user_id:
4075 if local_user_id:
4082 query = query.filter(cls.local_user_id == local_user_id)
4076 query = query.filter(cls.local_user_id == local_user_id)
4083 return query.first()
4077 return query.first()
4084
4078
4085 @classmethod
4079 @classmethod
4086 def user_by_external_id_and_provider(cls, external_id, provider_name):
4080 def user_by_external_id_and_provider(cls, external_id, provider_name):
4087 """
4081 """
4088 Returns User instance based on search params
4082 Returns User instance based on search params
4089
4083
4090 :param external_id:
4084 :param external_id:
4091 :param provider_name:
4085 :param provider_name:
4092 :return: User
4086 :return: User
4093 """
4087 """
4094 query = User.query()
4088 query = User.query()
4095 query = query.filter(cls.external_id == external_id)
4089 query = query.filter(cls.external_id == external_id)
4096 query = query.filter(cls.provider_name == provider_name)
4090 query = query.filter(cls.provider_name == provider_name)
4097 query = query.filter(User.user_id == cls.local_user_id)
4091 query = query.filter(User.user_id == cls.local_user_id)
4098 return query.first()
4092 return query.first()
4099
4093
4100 @classmethod
4094 @classmethod
4101 def by_local_user_id(cls, local_user_id):
4095 def by_local_user_id(cls, local_user_id):
4102 """
4096 """
4103 Returns all tokens for user
4097 Returns all tokens for user
4104
4098
4105 :param local_user_id:
4099 :param local_user_id:
4106 :return: ExternalIdentity
4100 :return: ExternalIdentity
4107 """
4101 """
4108 query = cls.query()
4102 query = cls.query()
4109 query = query.filter(cls.local_user_id == local_user_id)
4103 query = query.filter(cls.local_user_id == local_user_id)
4110 return query
4104 return query
4111
4105
4112
4106
4113 class Integration(Base, BaseModel):
4107 class Integration(Base, BaseModel):
4114 __tablename__ = 'integrations'
4108 __tablename__ = 'integrations'
4115 __table_args__ = (
4109 __table_args__ = (
4116 base_table_args
4110 base_table_args
4117 )
4111 )
4118
4112
4119 integration_id = Column('integration_id', Integer(), primary_key=True)
4113 integration_id = Column('integration_id', Integer(), primary_key=True)
4120 integration_type = Column('integration_type', String(255))
4114 integration_type = Column('integration_type', String(255))
4121 enabled = Column('enabled', Boolean(), nullable=False)
4115 enabled = Column('enabled', Boolean(), nullable=False)
4122 name = Column('name', String(255), nullable=False)
4116 name = Column('name', String(255), nullable=False)
4123 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4117 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4124 default=False)
4118 default=False)
4125
4119
4126 settings = Column(
4120 settings = Column(
4127 'settings_json', MutationObj.as_mutable(
4121 'settings_json', MutationObj.as_mutable(
4128 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4122 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4129 repo_id = Column(
4123 repo_id = Column(
4130 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4124 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4131 nullable=True, unique=None, default=None)
4125 nullable=True, unique=None, default=None)
4132 repo = relationship('Repository', lazy='joined')
4126 repo = relationship('Repository', lazy='joined')
4133
4127
4134 repo_group_id = Column(
4128 repo_group_id = Column(
4135 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4129 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4136 nullable=True, unique=None, default=None)
4130 nullable=True, unique=None, default=None)
4137 repo_group = relationship('RepoGroup', lazy='joined')
4131 repo_group = relationship('RepoGroup', lazy='joined')
4138
4132
4139 @property
4133 @property
4140 def scope(self):
4134 def scope(self):
4141 if self.repo:
4135 if self.repo:
4142 return repr(self.repo)
4136 return repr(self.repo)
4143 if self.repo_group:
4137 if self.repo_group:
4144 if self.child_repos_only:
4138 if self.child_repos_only:
4145 return repr(self.repo_group) + ' (child repos only)'
4139 return repr(self.repo_group) + ' (child repos only)'
4146 else:
4140 else:
4147 return repr(self.repo_group) + ' (recursive)'
4141 return repr(self.repo_group) + ' (recursive)'
4148 if self.child_repos_only:
4142 if self.child_repos_only:
4149 return 'root_repos'
4143 return 'root_repos'
4150 return 'global'
4144 return 'global'
4151
4145
4152 def __repr__(self):
4146 def __repr__(self):
4153 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4147 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4154
4148
4155
4149
4156 class RepoReviewRuleUser(Base, BaseModel):
4150 class RepoReviewRuleUser(Base, BaseModel):
4157 __tablename__ = 'repo_review_rules_users'
4151 __tablename__ = 'repo_review_rules_users'
4158 __table_args__ = (
4152 __table_args__ = (
4159 base_table_args
4153 base_table_args
4160 )
4154 )
4161
4155
4162 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4156 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4163 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4157 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4158 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4165 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4159 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4166 user = relationship('User')
4160 user = relationship('User')
4167
4161
4168 def rule_data(self):
4162 def rule_data(self):
4169 return {
4163 return {
4170 'mandatory': self.mandatory
4164 'mandatory': self.mandatory
4171 }
4165 }
4172
4166
4173
4167
4174 class RepoReviewRuleUserGroup(Base, BaseModel):
4168 class RepoReviewRuleUserGroup(Base, BaseModel):
4175 __tablename__ = 'repo_review_rules_users_groups'
4169 __tablename__ = 'repo_review_rules_users_groups'
4176 __table_args__ = (
4170 __table_args__ = (
4177 base_table_args
4171 base_table_args
4178 )
4172 )
4179
4173
4180 VOTE_RULE_ALL = -1
4174 VOTE_RULE_ALL = -1
4181
4175
4182 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4176 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4183 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4177 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4184 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4178 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4185 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4179 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4186 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4180 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4187 users_group = relationship('UserGroup')
4181 users_group = relationship('UserGroup')
4188
4182
4189 def rule_data(self):
4183 def rule_data(self):
4190 return {
4184 return {
4191 'mandatory': self.mandatory,
4185 'mandatory': self.mandatory,
4192 'vote_rule': self.vote_rule
4186 'vote_rule': self.vote_rule
4193 }
4187 }
4194
4188
4195 @property
4189 @property
4196 def vote_rule_label(self):
4190 def vote_rule_label(self):
4197 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4191 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4198 return 'all must vote'
4192 return 'all must vote'
4199 else:
4193 else:
4200 return 'min. vote {}'.format(self.vote_rule)
4194 return 'min. vote {}'.format(self.vote_rule)
4201
4195
4202
4196
4203 class RepoReviewRule(Base, BaseModel):
4197 class RepoReviewRule(Base, BaseModel):
4204 __tablename__ = 'repo_review_rules'
4198 __tablename__ = 'repo_review_rules'
4205 __table_args__ = (
4199 __table_args__ = (
4206 base_table_args
4200 base_table_args
4207 )
4201 )
4208
4202
4209 repo_review_rule_id = Column(
4203 repo_review_rule_id = Column(
4210 'repo_review_rule_id', Integer(), primary_key=True)
4204 'repo_review_rule_id', Integer(), primary_key=True)
4211 repo_id = Column(
4205 repo_id = Column(
4212 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4206 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4213 repo = relationship('Repository', backref='review_rules')
4207 repo = relationship('Repository', backref='review_rules')
4214
4208
4215 review_rule_name = Column('review_rule_name', String(255))
4209 review_rule_name = Column('review_rule_name', String(255))
4216 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4210 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4217 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4211 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4218 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4212 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4219
4213
4220 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4214 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4221 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4215 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4222 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4216 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4223 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4217 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4224
4218
4225 rule_users = relationship('RepoReviewRuleUser')
4219 rule_users = relationship('RepoReviewRuleUser')
4226 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4220 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4227
4221
4228 def _validate_pattern(self, value):
4222 def _validate_pattern(self, value):
4229 re.compile('^' + glob2re(value) + '$')
4223 re.compile('^' + glob2re(value) + '$')
4230
4224
4231 @hybrid_property
4225 @hybrid_property
4232 def source_branch_pattern(self):
4226 def source_branch_pattern(self):
4233 return self._branch_pattern or '*'
4227 return self._branch_pattern or '*'
4234
4228
4235 @source_branch_pattern.setter
4229 @source_branch_pattern.setter
4236 def source_branch_pattern(self, value):
4230 def source_branch_pattern(self, value):
4237 self._validate_pattern(value)
4231 self._validate_pattern(value)
4238 self._branch_pattern = value or '*'
4232 self._branch_pattern = value or '*'
4239
4233
4240 @hybrid_property
4234 @hybrid_property
4241 def target_branch_pattern(self):
4235 def target_branch_pattern(self):
4242 return self._target_branch_pattern or '*'
4236 return self._target_branch_pattern or '*'
4243
4237
4244 @target_branch_pattern.setter
4238 @target_branch_pattern.setter
4245 def target_branch_pattern(self, value):
4239 def target_branch_pattern(self, value):
4246 self._validate_pattern(value)
4240 self._validate_pattern(value)
4247 self._target_branch_pattern = value or '*'
4241 self._target_branch_pattern = value or '*'
4248
4242
4249 @hybrid_property
4243 @hybrid_property
4250 def file_pattern(self):
4244 def file_pattern(self):
4251 return self._file_pattern or '*'
4245 return self._file_pattern or '*'
4252
4246
4253 @file_pattern.setter
4247 @file_pattern.setter
4254 def file_pattern(self, value):
4248 def file_pattern(self, value):
4255 self._validate_pattern(value)
4249 self._validate_pattern(value)
4256 self._file_pattern = value or '*'
4250 self._file_pattern = value or '*'
4257
4251
4258 def matches(self, source_branch, target_branch, files_changed):
4252 def matches(self, source_branch, target_branch, files_changed):
4259 """
4253 """
4260 Check if this review rule matches a branch/files in a pull request
4254 Check if this review rule matches a branch/files in a pull request
4261
4255
4262 :param source_branch: source branch name for the commit
4256 :param source_branch: source branch name for the commit
4263 :param target_branch: target branch name for the commit
4257 :param target_branch: target branch name for the commit
4264 :param files_changed: list of file paths changed in the pull request
4258 :param files_changed: list of file paths changed in the pull request
4265 """
4259 """
4266
4260
4267 source_branch = source_branch or ''
4261 source_branch = source_branch or ''
4268 target_branch = target_branch or ''
4262 target_branch = target_branch or ''
4269 files_changed = files_changed or []
4263 files_changed = files_changed or []
4270
4264
4271 branch_matches = True
4265 branch_matches = True
4272 if source_branch or target_branch:
4266 if source_branch or target_branch:
4273 if self.source_branch_pattern == '*':
4267 if self.source_branch_pattern == '*':
4274 source_branch_match = True
4268 source_branch_match = True
4275 else:
4269 else:
4276 if self.source_branch_pattern.startswith('re:'):
4270 if self.source_branch_pattern.startswith('re:'):
4277 source_pattern = self.source_branch_pattern[3:]
4271 source_pattern = self.source_branch_pattern[3:]
4278 else:
4272 else:
4279 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4273 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4280 source_branch_regex = re.compile(source_pattern)
4274 source_branch_regex = re.compile(source_pattern)
4281 source_branch_match = bool(source_branch_regex.search(source_branch))
4275 source_branch_match = bool(source_branch_regex.search(source_branch))
4282 if self.target_branch_pattern == '*':
4276 if self.target_branch_pattern == '*':
4283 target_branch_match = True
4277 target_branch_match = True
4284 else:
4278 else:
4285 if self.target_branch_pattern.startswith('re:'):
4279 if self.target_branch_pattern.startswith('re:'):
4286 target_pattern = self.target_branch_pattern[3:]
4280 target_pattern = self.target_branch_pattern[3:]
4287 else:
4281 else:
4288 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4282 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4289 target_branch_regex = re.compile(target_pattern)
4283 target_branch_regex = re.compile(target_pattern)
4290 target_branch_match = bool(target_branch_regex.search(target_branch))
4284 target_branch_match = bool(target_branch_regex.search(target_branch))
4291
4285
4292 branch_matches = source_branch_match and target_branch_match
4286 branch_matches = source_branch_match and target_branch_match
4293
4287
4294 files_matches = True
4288 files_matches = True
4295 if self.file_pattern != '*':
4289 if self.file_pattern != '*':
4296 files_matches = False
4290 files_matches = False
4297 if self.file_pattern.startswith('re:'):
4291 if self.file_pattern.startswith('re:'):
4298 file_pattern = self.file_pattern[3:]
4292 file_pattern = self.file_pattern[3:]
4299 else:
4293 else:
4300 file_pattern = glob2re(self.file_pattern)
4294 file_pattern = glob2re(self.file_pattern)
4301 file_regex = re.compile(file_pattern)
4295 file_regex = re.compile(file_pattern)
4302 for filename in files_changed:
4296 for filename in files_changed:
4303 if file_regex.search(filename):
4297 if file_regex.search(filename):
4304 files_matches = True
4298 files_matches = True
4305 break
4299 break
4306
4300
4307 return branch_matches and files_matches
4301 return branch_matches and files_matches
4308
4302
4309 @property
4303 @property
4310 def review_users(self):
4304 def review_users(self):
4311 """ Returns the users which this rule applies to """
4305 """ Returns the users which this rule applies to """
4312
4306
4313 users = collections.OrderedDict()
4307 users = collections.OrderedDict()
4314
4308
4315 for rule_user in self.rule_users:
4309 for rule_user in self.rule_users:
4316 if rule_user.user.active:
4310 if rule_user.user.active:
4317 if rule_user.user not in users:
4311 if rule_user.user not in users:
4318 users[rule_user.user.username] = {
4312 users[rule_user.user.username] = {
4319 'user': rule_user.user,
4313 'user': rule_user.user,
4320 'source': 'user',
4314 'source': 'user',
4321 'source_data': {},
4315 'source_data': {},
4322 'data': rule_user.rule_data()
4316 'data': rule_user.rule_data()
4323 }
4317 }
4324
4318
4325 for rule_user_group in self.rule_user_groups:
4319 for rule_user_group in self.rule_user_groups:
4326 source_data = {
4320 source_data = {
4327 'user_group_id': rule_user_group.users_group.users_group_id,
4321 'user_group_id': rule_user_group.users_group.users_group_id,
4328 'name': rule_user_group.users_group.users_group_name,
4322 'name': rule_user_group.users_group.users_group_name,
4329 'members': len(rule_user_group.users_group.members)
4323 'members': len(rule_user_group.users_group.members)
4330 }
4324 }
4331 for member in rule_user_group.users_group.members:
4325 for member in rule_user_group.users_group.members:
4332 if member.user.active:
4326 if member.user.active:
4333 key = member.user.username
4327 key = member.user.username
4334 if key in users:
4328 if key in users:
4335 # skip this member as we have him already
4329 # skip this member as we have him already
4336 # this prevents from override the "first" matched
4330 # this prevents from override the "first" matched
4337 # users with duplicates in multiple groups
4331 # users with duplicates in multiple groups
4338 continue
4332 continue
4339
4333
4340 users[key] = {
4334 users[key] = {
4341 'user': member.user,
4335 'user': member.user,
4342 'source': 'user_group',
4336 'source': 'user_group',
4343 'source_data': source_data,
4337 'source_data': source_data,
4344 'data': rule_user_group.rule_data()
4338 'data': rule_user_group.rule_data()
4345 }
4339 }
4346
4340
4347 return users
4341 return users
4348
4342
4349 def user_group_vote_rule(self):
4343 def user_group_vote_rule(self):
4350 rules = []
4344 rules = []
4351 if self.rule_user_groups:
4345 if self.rule_user_groups:
4352 for user_group in self.rule_user_groups:
4346 for user_group in self.rule_user_groups:
4353 rules.append(user_group)
4347 rules.append(user_group)
4354 return rules
4348 return rules
4355
4349
4356 def __repr__(self):
4350 def __repr__(self):
4357 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4351 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4358 self.repo_review_rule_id, self.repo)
4352 self.repo_review_rule_id, self.repo)
4359
4353
4360
4354
4361 class ScheduleEntry(Base, BaseModel):
4355 class ScheduleEntry(Base, BaseModel):
4362 __tablename__ = 'schedule_entries'
4356 __tablename__ = 'schedule_entries'
4363 __table_args__ = (
4357 __table_args__ = (
4364 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4358 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4365 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4359 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4366 base_table_args,
4360 base_table_args,
4367 )
4361 )
4368
4362
4369 schedule_types = ['crontab', 'timedelta', 'integer']
4363 schedule_types = ['crontab', 'timedelta', 'integer']
4370 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4364 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4371
4365
4372 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4366 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4373 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4367 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4374 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4368 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4375
4369
4376 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4370 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4377 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4371 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4378
4372
4379 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4373 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4380 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4374 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4381
4375
4382 # task
4376 # task
4383 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4377 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4384 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4378 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4385 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4379 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4386 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4380 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4387
4381
4388 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4382 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4389 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4383 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4390
4384
4391 @hybrid_property
4385 @hybrid_property
4392 def schedule_type(self):
4386 def schedule_type(self):
4393 return self._schedule_type
4387 return self._schedule_type
4394
4388
4395 @schedule_type.setter
4389 @schedule_type.setter
4396 def schedule_type(self, val):
4390 def schedule_type(self, val):
4397 if val not in self.schedule_types:
4391 if val not in self.schedule_types:
4398 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4392 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4399 val, self.schedule_type))
4393 val, self.schedule_type))
4400
4394
4401 self._schedule_type = val
4395 self._schedule_type = val
4402
4396
4403 @classmethod
4397 @classmethod
4404 def get_uid(cls, obj):
4398 def get_uid(cls, obj):
4405 args = obj.task_args
4399 args = obj.task_args
4406 kwargs = obj.task_kwargs
4400 kwargs = obj.task_kwargs
4407 if isinstance(args, JsonRaw):
4401 if isinstance(args, JsonRaw):
4408 try:
4402 try:
4409 args = json.loads(args)
4403 args = json.loads(args)
4410 except ValueError:
4404 except ValueError:
4411 args = tuple()
4405 args = tuple()
4412
4406
4413 if isinstance(kwargs, JsonRaw):
4407 if isinstance(kwargs, JsonRaw):
4414 try:
4408 try:
4415 kwargs = json.loads(kwargs)
4409 kwargs = json.loads(kwargs)
4416 except ValueError:
4410 except ValueError:
4417 kwargs = dict()
4411 kwargs = dict()
4418
4412
4419 dot_notation = obj.task_dot_notation
4413 dot_notation = obj.task_dot_notation
4420 val = '.'.join(map(safe_str, [
4414 val = '.'.join(map(safe_str, [
4421 sorted(dot_notation), args, sorted(kwargs.items())]))
4415 sorted(dot_notation), args, sorted(kwargs.items())]))
4422 return hashlib.sha1(val).hexdigest()
4416 return hashlib.sha1(val).hexdigest()
4423
4417
4424 @classmethod
4418 @classmethod
4425 def get_by_schedule_name(cls, schedule_name):
4419 def get_by_schedule_name(cls, schedule_name):
4426 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4420 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4427
4421
4428 @classmethod
4422 @classmethod
4429 def get_by_schedule_id(cls, schedule_id):
4423 def get_by_schedule_id(cls, schedule_id):
4430 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4424 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4431
4425
4432 @property
4426 @property
4433 def task(self):
4427 def task(self):
4434 return self.task_dot_notation
4428 return self.task_dot_notation
4435
4429
4436 @property
4430 @property
4437 def schedule(self):
4431 def schedule(self):
4438 from rhodecode.lib.celerylib.utils import raw_2_schedule
4432 from rhodecode.lib.celerylib.utils import raw_2_schedule
4439 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4433 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4440 return schedule
4434 return schedule
4441
4435
4442 @property
4436 @property
4443 def args(self):
4437 def args(self):
4444 try:
4438 try:
4445 return list(self.task_args or [])
4439 return list(self.task_args or [])
4446 except ValueError:
4440 except ValueError:
4447 return list()
4441 return list()
4448
4442
4449 @property
4443 @property
4450 def kwargs(self):
4444 def kwargs(self):
4451 try:
4445 try:
4452 return dict(self.task_kwargs or {})
4446 return dict(self.task_kwargs or {})
4453 except ValueError:
4447 except ValueError:
4454 return dict()
4448 return dict()
4455
4449
4456 def _as_raw(self, val):
4450 def _as_raw(self, val):
4457 if hasattr(val, 'de_coerce'):
4451 if hasattr(val, 'de_coerce'):
4458 val = val.de_coerce()
4452 val = val.de_coerce()
4459 if val:
4453 if val:
4460 val = json.dumps(val)
4454 val = json.dumps(val)
4461
4455
4462 return val
4456 return val
4463
4457
4464 @property
4458 @property
4465 def schedule_definition_raw(self):
4459 def schedule_definition_raw(self):
4466 return self._as_raw(self.schedule_definition)
4460 return self._as_raw(self.schedule_definition)
4467
4461
4468 @property
4462 @property
4469 def args_raw(self):
4463 def args_raw(self):
4470 return self._as_raw(self.task_args)
4464 return self._as_raw(self.task_args)
4471
4465
4472 @property
4466 @property
4473 def kwargs_raw(self):
4467 def kwargs_raw(self):
4474 return self._as_raw(self.task_kwargs)
4468 return self._as_raw(self.task_kwargs)
4475
4469
4476 def __repr__(self):
4470 def __repr__(self):
4477 return '<DB:ScheduleEntry({}:{})>'.format(
4471 return '<DB:ScheduleEntry({}:{})>'.format(
4478 self.schedule_entry_id, self.schedule_name)
4472 self.schedule_entry_id, self.schedule_name)
4479
4473
4480
4474
4481 @event.listens_for(ScheduleEntry, 'before_update')
4475 @event.listens_for(ScheduleEntry, 'before_update')
4482 def update_task_uid(mapper, connection, target):
4476 def update_task_uid(mapper, connection, target):
4483 target.task_uid = ScheduleEntry.get_uid(target)
4477 target.task_uid = ScheduleEntry.get_uid(target)
4484
4478
4485
4479
4486 @event.listens_for(ScheduleEntry, 'before_insert')
4480 @event.listens_for(ScheduleEntry, 'before_insert')
4487 def set_task_uid(mapper, connection, target):
4481 def set_task_uid(mapper, connection, target):
4488 target.task_uid = ScheduleEntry.get_uid(target)
4482 target.task_uid = ScheduleEntry.get_uid(target)
4489
4483
4490
4484
4491 class DbMigrateVersion(Base, BaseModel):
4485 class DbMigrateVersion(Base, BaseModel):
4492 __tablename__ = 'db_migrate_version'
4486 __tablename__ = 'db_migrate_version'
4493 __table_args__ = (
4487 __table_args__ = (
4494 base_table_args,
4488 base_table_args,
4495 )
4489 )
4496
4490
4497 repository_id = Column('repository_id', String(250), primary_key=True)
4491 repository_id = Column('repository_id', String(250), primary_key=True)
4498 repository_path = Column('repository_path', Text)
4492 repository_path = Column('repository_path', Text)
4499 version = Column('version', Integer)
4493 version = Column('version', Integer)
4500
4494
4501 @classmethod
4495 @classmethod
4502 def set_version(cls, version):
4496 def set_version(cls, version):
4503 """
4497 """
4504 Helper for forcing a different version, usually for debugging purposes via ishell.
4498 Helper for forcing a different version, usually for debugging purposes via ishell.
4505 """
4499 """
4506 ver = DbMigrateVersion.query().first()
4500 ver = DbMigrateVersion.query().first()
4507 ver.version = version
4501 ver.version = version
4508 Session().commit()
4502 Session().commit()
4509
4503
4510
4504
4511 class DbSession(Base, BaseModel):
4505 class DbSession(Base, BaseModel):
4512 __tablename__ = 'db_session'
4506 __tablename__ = 'db_session'
4513 __table_args__ = (
4507 __table_args__ = (
4514 base_table_args,
4508 base_table_args,
4515 )
4509 )
4516
4510
4517 def __repr__(self):
4511 def __repr__(self):
4518 return '<DB:DbSession({})>'.format(self.id)
4512 return '<DB:DbSession({})>'.format(self.id)
4519
4513
4520 id = Column('id', Integer())
4514 id = Column('id', Integer())
4521 namespace = Column('namespace', String(255), primary_key=True)
4515 namespace = Column('namespace', String(255), primary_key=True)
4522 accessed = Column('accessed', DateTime, nullable=False)
4516 accessed = Column('accessed', DateTime, nullable=False)
4523 created = Column('created', DateTime, nullable=False)
4517 created = Column('created', DateTime, nullable=False)
4524 data = Column('data', PickleType, nullable=False)
4518 data = Column('data', PickleType, nullable=False)
4525
4519
4526
4520
4527 class BeakerCache(Base, BaseModel):
4521 class BeakerCache(Base, BaseModel):
4528 __tablename__ = 'beaker_cache'
4522 __tablename__ = 'beaker_cache'
4529 __table_args__ = (
4523 __table_args__ = (
4530 base_table_args,
4524 base_table_args,
4531 )
4525 )
4532
4526
4533 def __repr__(self):
4527 def __repr__(self):
4534 return '<DB:DbSession({})>'.format(self.id)
4528 return '<DB:DbSession({})>'.format(self.id)
4535
4529
4536 id = Column('id', Integer())
4530 id = Column('id', Integer())
4537 namespace = Column('namespace', String(255), primary_key=True)
4531 namespace = Column('namespace', String(255), primary_key=True)
4538 accessed = Column('accessed', DateTime, nullable=False)
4532 accessed = Column('accessed', DateTime, nullable=False)
4539 created = Column('created', DateTime, nullable=False)
4533 created = Column('created', DateTime, nullable=False)
4540 data = Column('data', PickleType, nullable=False)
4534 data = Column('data', PickleType, nullable=False)
@@ -1,328 +1,329 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 import io
20 import io
21 import re
21 import re
22 import datetime
22 import datetime
23 import logging
23 import logging
24 import Queue
24 import Queue
25 import subprocess32
25 import subprocess32
26 import os
26 import os
27
27
28
28
29 from dateutil.parser import parse
29 from dateutil.parser import parse
30 from pyramid.i18n import get_localizer
30 from pyramid.i18n import get_localizer
31 from pyramid.threadlocal import get_current_request
31 from pyramid.threadlocal import get_current_request
32 from pyramid.interfaces import IRoutesMapper
32 from pyramid.interfaces import IRoutesMapper
33 from pyramid.settings import asbool
33 from pyramid.settings import asbool
34 from pyramid.path import AssetResolver
34 from pyramid.path import AssetResolver
35 from threading import Thread
35 from threading import Thread
36
36
37 from rhodecode.translation import _ as tsf
37 from rhodecode.translation import _ as tsf
38 from rhodecode.config.jsroutes import generate_jsroutes_content
38 from rhodecode.config.jsroutes import generate_jsroutes_content
39 from rhodecode.lib import auth
39 from rhodecode.lib import auth
40 from rhodecode.lib.base import get_auth_user
40 from rhodecode.lib.base import get_auth_user
41
41
42
42
43 import rhodecode
43 import rhodecode
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 def add_renderer_globals(event):
49 def add_renderer_globals(event):
50 from rhodecode.lib import helpers
50 from rhodecode.lib import helpers
51
51
52 # TODO: When executed in pyramid view context the request is not available
52 # TODO: When executed in pyramid view context the request is not available
53 # in the event. Find a better solution to get the request.
53 # in the event. Find a better solution to get the request.
54 request = event['request'] or get_current_request()
54 request = event['request'] or get_current_request()
55
55
56 # Add Pyramid translation as '_' to context
56 # Add Pyramid translation as '_' to context
57 event['_'] = request.translate
57 event['_'] = request.translate
58 event['_ungettext'] = request.plularize
58 event['_ungettext'] = request.plularize
59 event['h'] = helpers
59 event['h'] = helpers
60
60
61
61
62 def add_localizer(event):
62 def add_localizer(event):
63 request = event.request
63 request = event.request
64 localizer = request.localizer
64 localizer = request.localizer
65
65
66 def auto_translate(*args, **kwargs):
66 def auto_translate(*args, **kwargs):
67 return localizer.translate(tsf(*args, **kwargs))
67 return localizer.translate(tsf(*args, **kwargs))
68
68
69 request.translate = auto_translate
69 request.translate = auto_translate
70 request.plularize = localizer.pluralize
70 request.plularize = localizer.pluralize
71
71
72
72
73 def set_user_lang(event):
73 def set_user_lang(event):
74 request = event.request
74 request = event.request
75 cur_user = getattr(request, 'user', None)
75 cur_user = getattr(request, 'user', None)
76
76
77 if cur_user:
77 if cur_user:
78 user_lang = cur_user.get_instance().user_data.get('language')
78 user_lang = cur_user.get_instance().user_data.get('language')
79 if user_lang:
79 if user_lang:
80 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
80 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
81 event.request._LOCALE_ = user_lang
81 event.request._LOCALE_ = user_lang
82
82
83
83
84 def add_request_user_context(event):
84 def add_request_user_context(event):
85 """
85 """
86 Adds auth user into request context
86 Adds auth user into request context
87 """
87 """
88 request = event.request
88 request = event.request
89 # access req_id as soon as possible
89 # access req_id as soon as possible
90 req_id = request.req_id
90 req_id = request.req_id
91
91
92 if hasattr(request, 'vcs_call'):
92 if hasattr(request, 'vcs_call'):
93 # skip vcs calls
93 # skip vcs calls
94 return
94 return
95
95
96 if hasattr(request, 'rpc_method'):
96 if hasattr(request, 'rpc_method'):
97 # skip api calls
97 # skip api calls
98 return
98 return
99
99
100 auth_user = get_auth_user(request)
100 auth_user = get_auth_user(request)
101 request.user = auth_user
101 request.user = auth_user
102 request.environ['rc_auth_user'] = auth_user
102 request.environ['rc_auth_user'] = auth_user
103 request.environ['rc_auth_user_id'] = auth_user.user_id
103 request.environ['rc_req_id'] = req_id
104 request.environ['rc_req_id'] = req_id
104
105
105
106
106 def inject_app_settings(event):
107 def inject_app_settings(event):
107 settings = event.app.registry.settings
108 settings = event.app.registry.settings
108 # inject info about available permissions
109 # inject info about available permissions
109 auth.set_available_permissions(settings)
110 auth.set_available_permissions(settings)
110
111
111
112
112 def scan_repositories_if_enabled(event):
113 def scan_repositories_if_enabled(event):
113 """
114 """
114 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
115 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
115 does a repository scan if enabled in the settings.
116 does a repository scan if enabled in the settings.
116 """
117 """
117 settings = event.app.registry.settings
118 settings = event.app.registry.settings
118 vcs_server_enabled = settings['vcs.server.enable']
119 vcs_server_enabled = settings['vcs.server.enable']
119 import_on_startup = settings['startup.import_repos']
120 import_on_startup = settings['startup.import_repos']
120 if vcs_server_enabled and import_on_startup:
121 if vcs_server_enabled and import_on_startup:
121 from rhodecode.model.scm import ScmModel
122 from rhodecode.model.scm import ScmModel
122 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
123 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
123 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
124 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
124 repo2db_mapper(repositories, remove_obsolete=False)
125 repo2db_mapper(repositories, remove_obsolete=False)
125
126
126
127
127 def write_metadata_if_needed(event):
128 def write_metadata_if_needed(event):
128 """
129 """
129 Writes upgrade metadata
130 Writes upgrade metadata
130 """
131 """
131 import rhodecode
132 import rhodecode
132 from rhodecode.lib import system_info
133 from rhodecode.lib import system_info
133 from rhodecode.lib import ext_json
134 from rhodecode.lib import ext_json
134
135
135 fname = '.rcmetadata.json'
136 fname = '.rcmetadata.json'
136 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 metadata_destination = os.path.join(ini_loc, fname)
138 metadata_destination = os.path.join(ini_loc, fname)
138
139
139 def get_update_age():
140 def get_update_age():
140 now = datetime.datetime.utcnow()
141 now = datetime.datetime.utcnow()
141
142
142 with open(metadata_destination, 'rb') as f:
143 with open(metadata_destination, 'rb') as f:
143 data = ext_json.json.loads(f.read())
144 data = ext_json.json.loads(f.read())
144 if 'created_on' in data:
145 if 'created_on' in data:
145 update_date = parse(data['created_on'])
146 update_date = parse(data['created_on'])
146 diff = now - update_date
147 diff = now - update_date
147 return diff.total_seconds() / 60.0
148 return diff.total_seconds() / 60.0
148
149
149 return 0
150 return 0
150
151
151 def write():
152 def write():
152 configuration = system_info.SysInfo(
153 configuration = system_info.SysInfo(
153 system_info.rhodecode_config)()['value']
154 system_info.rhodecode_config)()['value']
154 license_token = configuration['config']['license_token']
155 license_token = configuration['config']['license_token']
155
156
156 setup = dict(
157 setup = dict(
157 workers=configuration['config']['server:main'].get(
158 workers=configuration['config']['server:main'].get(
158 'workers', '?'),
159 'workers', '?'),
159 worker_type=configuration['config']['server:main'].get(
160 worker_type=configuration['config']['server:main'].get(
160 'worker_class', 'sync'),
161 'worker_class', 'sync'),
161 )
162 )
162 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 del dbinfo['url']
164 del dbinfo['url']
164
165
165 metadata = dict(
166 metadata = dict(
166 desc='upgrade metadata info',
167 desc='upgrade metadata info',
167 license_token=license_token,
168 license_token=license_token,
168 created_on=datetime.datetime.utcnow().isoformat(),
169 created_on=datetime.datetime.utcnow().isoformat(),
169 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 database=dbinfo,
172 database=dbinfo,
172 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 memory=system_info.SysInfo(system_info.memory)()['value'],
174 memory=system_info.SysInfo(system_info.memory)()['value'],
174 setup=setup
175 setup=setup
175 )
176 )
176
177
177 with open(metadata_destination, 'wb') as f:
178 with open(metadata_destination, 'wb') as f:
178 f.write(ext_json.json.dumps(metadata))
179 f.write(ext_json.json.dumps(metadata))
179
180
180 settings = event.app.registry.settings
181 settings = event.app.registry.settings
181 if settings.get('metadata.skip'):
182 if settings.get('metadata.skip'):
182 return
183 return
183
184
184 # only write this every 24h, workers restart caused unwanted delays
185 # only write this every 24h, workers restart caused unwanted delays
185 try:
186 try:
186 age_in_min = get_update_age()
187 age_in_min = get_update_age()
187 except Exception:
188 except Exception:
188 age_in_min = 0
189 age_in_min = 0
189
190
190 if age_in_min > 60 * 60 * 24:
191 if age_in_min > 60 * 60 * 24:
191 return
192 return
192
193
193 try:
194 try:
194 write()
195 write()
195 except Exception:
196 except Exception:
196 pass
197 pass
197
198
198
199
199 def write_js_routes_if_enabled(event):
200 def write_js_routes_if_enabled(event):
200 registry = event.app.registry
201 registry = event.app.registry
201
202
202 mapper = registry.queryUtility(IRoutesMapper)
203 mapper = registry.queryUtility(IRoutesMapper)
203 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204
205
205 def _extract_route_information(route):
206 def _extract_route_information(route):
206 """
207 """
207 Convert a route into tuple(name, path, args), eg:
208 Convert a route into tuple(name, path, args), eg:
208 ('show_user', '/profile/%(username)s', ['username'])
209 ('show_user', '/profile/%(username)s', ['username'])
209 """
210 """
210
211
211 routepath = route.pattern
212 routepath = route.pattern
212 pattern = route.pattern
213 pattern = route.pattern
213
214
214 def replace(matchobj):
215 def replace(matchobj):
215 if matchobj.group(1):
216 if matchobj.group(1):
216 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 else:
218 else:
218 return "%%(%s)s" % matchobj.group(2)
219 return "%%(%s)s" % matchobj.group(2)
219
220
220 routepath = _argument_prog.sub(replace, routepath)
221 routepath = _argument_prog.sub(replace, routepath)
221
222
222 if not routepath.startswith('/'):
223 if not routepath.startswith('/'):
223 routepath = '/'+routepath
224 routepath = '/'+routepath
224
225
225 return (
226 return (
226 route.name,
227 route.name,
227 routepath,
228 routepath,
228 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 for arg in _argument_prog.findall(pattern)]
230 for arg in _argument_prog.findall(pattern)]
230 )
231 )
231
232
232 def get_routes():
233 def get_routes():
233 # pyramid routes
234 # pyramid routes
234 for route in mapper.get_routes():
235 for route in mapper.get_routes():
235 if not route.name.startswith('__'):
236 if not route.name.startswith('__'):
236 yield _extract_route_information(route)
237 yield _extract_route_information(route)
237
238
238 if asbool(registry.settings.get('generate_js_files', 'false')):
239 if asbool(registry.settings.get('generate_js_files', 'false')):
239 static_path = AssetResolver().resolve('rhodecode:public').abspath()
240 static_path = AssetResolver().resolve('rhodecode:public').abspath()
240 jsroutes = get_routes()
241 jsroutes = get_routes()
241 jsroutes_file_content = generate_jsroutes_content(jsroutes)
242 jsroutes_file_content = generate_jsroutes_content(jsroutes)
242 jsroutes_file_path = os.path.join(
243 jsroutes_file_path = os.path.join(
243 static_path, 'js', 'rhodecode', 'routes.js')
244 static_path, 'js', 'rhodecode', 'routes.js')
244
245
245 try:
246 try:
246 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
247 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
247 f.write(jsroutes_file_content)
248 f.write(jsroutes_file_content)
248 except Exception:
249 except Exception:
249 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
250 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
250
251
251
252
252 class Subscriber(object):
253 class Subscriber(object):
253 """
254 """
254 Base class for subscribers to the pyramid event system.
255 Base class for subscribers to the pyramid event system.
255 """
256 """
256 def __call__(self, event):
257 def __call__(self, event):
257 self.run(event)
258 self.run(event)
258
259
259 def run(self, event):
260 def run(self, event):
260 raise NotImplementedError('Subclass has to implement this.')
261 raise NotImplementedError('Subclass has to implement this.')
261
262
262
263
263 class AsyncSubscriber(Subscriber):
264 class AsyncSubscriber(Subscriber):
264 """
265 """
265 Subscriber that handles the execution of events in a separate task to not
266 Subscriber that handles the execution of events in a separate task to not
266 block the execution of the code which triggers the event. It puts the
267 block the execution of the code which triggers the event. It puts the
267 received events into a queue from which the worker process takes them in
268 received events into a queue from which the worker process takes them in
268 order.
269 order.
269 """
270 """
270 def __init__(self):
271 def __init__(self):
271 self._stop = False
272 self._stop = False
272 self._eventq = Queue.Queue()
273 self._eventq = Queue.Queue()
273 self._worker = self.create_worker()
274 self._worker = self.create_worker()
274 self._worker.start()
275 self._worker.start()
275
276
276 def __call__(self, event):
277 def __call__(self, event):
277 self._eventq.put(event)
278 self._eventq.put(event)
278
279
279 def create_worker(self):
280 def create_worker(self):
280 worker = Thread(target=self.do_work)
281 worker = Thread(target=self.do_work)
281 worker.daemon = True
282 worker.daemon = True
282 return worker
283 return worker
283
284
284 def stop_worker(self):
285 def stop_worker(self):
285 self._stop = False
286 self._stop = False
286 self._eventq.put(None)
287 self._eventq.put(None)
287 self._worker.join()
288 self._worker.join()
288
289
289 def do_work(self):
290 def do_work(self):
290 while not self._stop:
291 while not self._stop:
291 event = self._eventq.get()
292 event = self._eventq.get()
292 if event is not None:
293 if event is not None:
293 self.run(event)
294 self.run(event)
294
295
295
296
296 class AsyncSubprocessSubscriber(AsyncSubscriber):
297 class AsyncSubprocessSubscriber(AsyncSubscriber):
297 """
298 """
298 Subscriber that uses the subprocess32 module to execute a command if an
299 Subscriber that uses the subprocess32 module to execute a command if an
299 event is received. Events are handled asynchronously.
300 event is received. Events are handled asynchronously.
300 """
301 """
301
302
302 def __init__(self, cmd, timeout=None):
303 def __init__(self, cmd, timeout=None):
303 super(AsyncSubprocessSubscriber, self).__init__()
304 super(AsyncSubprocessSubscriber, self).__init__()
304 self._cmd = cmd
305 self._cmd = cmd
305 self._timeout = timeout
306 self._timeout = timeout
306
307
307 def run(self, event):
308 def run(self, event):
308 cmd = self._cmd
309 cmd = self._cmd
309 timeout = self._timeout
310 timeout = self._timeout
310 log.debug('Executing command %s.', cmd)
311 log.debug('Executing command %s.', cmd)
311
312
312 try:
313 try:
313 output = subprocess32.check_output(
314 output = subprocess32.check_output(
314 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
315 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
315 log.debug('Command finished %s', cmd)
316 log.debug('Command finished %s', cmd)
316 if output:
317 if output:
317 log.debug('Command output: %s', output)
318 log.debug('Command output: %s', output)
318 except subprocess32.TimeoutExpired as e:
319 except subprocess32.TimeoutExpired as e:
319 log.exception('Timeout while executing command.')
320 log.exception('Timeout while executing command.')
320 if e.output:
321 if e.output:
321 log.error('Command output: %s', e.output)
322 log.error('Command output: %s', e.output)
322 except subprocess32.CalledProcessError as e:
323 except subprocess32.CalledProcessError as e:
323 log.exception('Error while executing command.')
324 log.exception('Error while executing command.')
324 if e.output:
325 if e.output:
325 log.error('Command output: %s', e.output)
326 log.error('Command output: %s', e.output)
326 except:
327 except:
327 log.exception(
328 log.exception(
328 'Exception while executing command %s.', cmd)
329 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now