##// END OF EJS Templates
logging: use lazy parameter evaluation in log calls.
marcink -
r3061:a44afbe5 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,543 +1,542 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 request.environ['rc_auth_user_id'] = auth_u.user_id
212
212
213 # now check if token is valid for API
213 # now check if token is valid for API
214 auth_token = request.rpc_api_key
214 auth_token = request.rpc_api_key
215 token_match = api_user.authenticate_by_token(
215 token_match = api_user.authenticate_by_token(
216 auth_token, roles=[UserApiKeys.ROLE_API])
216 auth_token, roles=[UserApiKeys.ROLE_API])
217 invalid_token = not token_match
217 invalid_token = not token_match
218
218
219 log.debug('Checking if API KEY is valid with proper role')
219 log.debug('Checking if API KEY is valid with proper role')
220 if invalid_token:
220 if invalid_token:
221 return jsonrpc_error(
221 return jsonrpc_error(
222 request, retid=request.rpc_id,
222 request, retid=request.rpc_id,
223 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')
224
224
225 except Exception:
225 except Exception:
226 log.exception('Error on API AUTH')
226 log.exception('Error on API AUTH')
227 return jsonrpc_error(
227 return jsonrpc_error(
228 request, retid=request.rpc_id, message='Invalid API KEY')
228 request, retid=request.rpc_id, message='Invalid API KEY')
229
229
230 method = request.rpc_method
230 method = request.rpc_method
231 func = request.registry.jsonrpc_methods[method]
231 func = request.registry.jsonrpc_methods[method]
232
232
233 # now that we have a method, add request._req_params to
233 # now that we have a method, add request._req_params to
234 # self.kargs and dispatch control to WGIController
234 # self.kargs and dispatch control to WGIController
235 argspec = inspect.getargspec(func)
235 argspec = inspect.getargspec(func)
236 arglist = argspec[0]
236 arglist = argspec[0]
237 defaults = map(type, argspec[3] or [])
237 defaults = map(type, argspec[3] or [])
238 default_empty = types.NotImplementedType
238 default_empty = types.NotImplementedType
239
239
240 # kw arguments required by this method
240 # kw arguments required by this method
241 func_kwargs = dict(itertools.izip_longest(
241 func_kwargs = dict(itertools.izip_longest(
242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
243
243
244 # 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
245 # 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
246 user_var = 'apiuser'
246 user_var = 'apiuser'
247 request_var = 'request'
247 request_var = 'request'
248
248
249 for arg in [user_var, request_var]:
249 for arg in [user_var, request_var]:
250 if arg not in arglist:
250 if arg not in arglist:
251 return jsonrpc_error(
251 return jsonrpc_error(
252 request,
252 request,
253 retid=request.rpc_id,
253 retid=request.rpc_id,
254 message='This method [%s] does not support '
254 message='This method [%s] does not support '
255 'required parameter `%s`' % (func.__name__, arg))
255 'required parameter `%s`' % (func.__name__, arg))
256
256
257 # get our arglist and check if we provided them as args
257 # get our arglist and check if we provided them as args
258 for arg, default in func_kwargs.items():
258 for arg, default in func_kwargs.items():
259 if arg in [user_var, request_var]:
259 if arg in [user_var, request_var]:
260 # user_var and request_var are pre-hardcoded parameters and we
260 # user_var and request_var are pre-hardcoded parameters and we
261 # don't need to do any translation
261 # don't need to do any translation
262 continue
262 continue
263
263
264 # skip the required param check if it's default value is
264 # skip the required param check if it's default value is
265 # NotImplementedType (default_empty)
265 # NotImplementedType (default_empty)
266 if default == default_empty and arg not in request.rpc_params:
266 if default == default_empty and arg not in request.rpc_params:
267 return jsonrpc_error(
267 return jsonrpc_error(
268 request,
268 request,
269 retid=request.rpc_id,
269 retid=request.rpc_id,
270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
271 )
271 )
272
272
273 # sanitize extra passed arguments
273 # sanitize extra passed arguments
274 for k in request.rpc_params.keys()[:]:
274 for k in request.rpc_params.keys()[:]:
275 if k not in func_kwargs:
275 if k not in func_kwargs:
276 del request.rpc_params[k]
276 del request.rpc_params[k]
277
277
278 call_params = request.rpc_params
278 call_params = request.rpc_params
279 call_params.update({
279 call_params.update({
280 'request': request,
280 'request': request,
281 'apiuser': auth_u
281 'apiuser': auth_u
282 })
282 })
283
283
284 # register some common functions for usage
284 # register some common functions for usage
285 attach_context_attributes(
285 attach_context_attributes(
286 TemplateArgs(), request, request.rpc_user.user_id)
286 TemplateArgs(), request, request.rpc_user.user_id)
287
287
288 try:
288 try:
289 ret_value = func(**call_params)
289 ret_value = func(**call_params)
290 return jsonrpc_response(request, ret_value)
290 return jsonrpc_response(request, ret_value)
291 except JSONRPCBaseError:
291 except JSONRPCBaseError:
292 raise
292 raise
293 except Exception:
293 except Exception:
294 log.exception('Unhandled exception occurred on api call: %s', func)
294 log.exception('Unhandled exception occurred on api call: %s', func)
295 return jsonrpc_error(request, retid=request.rpc_id,
295 return jsonrpc_error(request, retid=request.rpc_id,
296 message='Internal server error')
296 message='Internal server error')
297
297
298
298
299 def setup_request(request):
299 def setup_request(request):
300 """
300 """
301 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
302 to validate and bootstrap requests for usage in rpc calls.
302 to validate and bootstrap requests for usage in rpc calls.
303
303
304 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
305 user.
305 user.
306 """
306 """
307
307
308 log.debug('Executing setup request: %r', request)
308 log.debug('Executing setup request: %r', request)
309 request.rpc_ip_addr = get_ip_addr(request.environ)
309 request.rpc_ip_addr = get_ip_addr(request.environ)
310 # TODO(marcink): deprecate GET at some point
310 # TODO(marcink): deprecate GET at some point
311 if request.method not in ['POST', 'GET']:
311 if request.method not in ['POST', 'GET']:
312 log.debug('unsupported request method "%s"', request.method)
312 log.debug('unsupported request method "%s"', request.method)
313 raise JSONRPCError(
313 raise JSONRPCError(
314 'unsupported request method "%s". Please use POST' % request.method)
314 'unsupported request method "%s". Please use POST' % request.method)
315
315
316 if 'CONTENT_LENGTH' not in request.environ:
316 if 'CONTENT_LENGTH' not in request.environ:
317 log.debug("No Content-Length")
317 log.debug("No Content-Length")
318 raise JSONRPCError("Empty body, No Content-Length in request")
318 raise JSONRPCError("Empty body, No Content-Length in request")
319
319
320 else:
320 else:
321 length = request.environ['CONTENT_LENGTH']
321 length = request.environ['CONTENT_LENGTH']
322 log.debug('Content-Length: %s', length)
322 log.debug('Content-Length: %s', length)
323
323
324 if length == 0:
324 if length == 0:
325 log.debug("Content-Length is 0")
325 log.debug("Content-Length is 0")
326 raise JSONRPCError("Content-Length is 0")
326 raise JSONRPCError("Content-Length is 0")
327
327
328 raw_body = request.body
328 raw_body = request.body
329 try:
329 try:
330 json_body = json.loads(raw_body)
330 json_body = json.loads(raw_body)
331 except ValueError as e:
331 except ValueError as e:
332 # catch JSON errors Here
332 # catch JSON errors Here
333 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))
334
334
335 request.rpc_id = json_body.get('id')
335 request.rpc_id = json_body.get('id')
336 request.rpc_method = json_body.get('method')
336 request.rpc_method = json_body.get('method')
337
337
338 # check required base parameters
338 # check required base parameters
339 try:
339 try:
340 api_key = json_body.get('api_key')
340 api_key = json_body.get('api_key')
341 if not api_key:
341 if not api_key:
342 api_key = json_body.get('auth_token')
342 api_key = json_body.get('auth_token')
343
343
344 if not api_key:
344 if not api_key:
345 raise KeyError('api_key or auth_token')
345 raise KeyError('api_key or auth_token')
346
346
347 # TODO(marcink): support passing in token in request header
347 # TODO(marcink): support passing in token in request header
348
348
349 request.rpc_api_key = api_key
349 request.rpc_api_key = api_key
350 request.rpc_id = json_body['id']
350 request.rpc_id = json_body['id']
351 request.rpc_method = json_body['method']
351 request.rpc_method = json_body['method']
352 request.rpc_params = json_body['args'] \
352 request.rpc_params = json_body['args'] \
353 if isinstance(json_body['args'], dict) else {}
353 if isinstance(json_body['args'], dict) else {}
354
354
355 log.debug(
355 log.debug('method: %s, params: %s', request.rpc_method, request.rpc_params)
356 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
357 except KeyError as e:
356 except KeyError as e:
358 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
359
358
360 log.debug('setup complete, now handling method:%s rpcid:%s',
359 log.debug('setup complete, now handling method:%s rpcid:%s',
361 request.rpc_method, request.rpc_id, )
360 request.rpc_method, request.rpc_id, )
362
361
363
362
364 class RoutePredicate(object):
363 class RoutePredicate(object):
365 def __init__(self, val, config):
364 def __init__(self, val, config):
366 self.val = val
365 self.val = val
367
366
368 def text(self):
367 def text(self):
369 return 'jsonrpc route = %s' % self.val
368 return 'jsonrpc route = %s' % self.val
370
369
371 phash = text
370 phash = text
372
371
373 def __call__(self, info, request):
372 def __call__(self, info, request):
374 if self.val:
373 if self.val:
375 # potentially setup and bootstrap our call
374 # potentially setup and bootstrap our call
376 setup_request(request)
375 setup_request(request)
377
376
378 # Always return True so that even if it isn't a valid RPC it
377 # Always return True so that even if it isn't a valid RPC it
379 # will fall through to the underlaying handlers like notfound_view
378 # will fall through to the underlaying handlers like notfound_view
380 return True
379 return True
381
380
382
381
383 class NotFoundPredicate(object):
382 class NotFoundPredicate(object):
384 def __init__(self, val, config):
383 def __init__(self, val, config):
385 self.val = val
384 self.val = val
386 self.methods = config.registry.jsonrpc_methods
385 self.methods = config.registry.jsonrpc_methods
387
386
388 def text(self):
387 def text(self):
389 return 'jsonrpc method not found = {}.'.format(self.val)
388 return 'jsonrpc method not found = {}.'.format(self.val)
390
389
391 phash = text
390 phash = text
392
391
393 def __call__(self, info, request):
392 def __call__(self, info, request):
394 return hasattr(request, 'rpc_method')
393 return hasattr(request, 'rpc_method')
395
394
396
395
397 class MethodPredicate(object):
396 class MethodPredicate(object):
398 def __init__(self, val, config):
397 def __init__(self, val, config):
399 self.method = val
398 self.method = val
400
399
401 def text(self):
400 def text(self):
402 return 'jsonrpc method = %s' % self.method
401 return 'jsonrpc method = %s' % self.method
403
402
404 phash = text
403 phash = text
405
404
406 def __call__(self, context, request):
405 def __call__(self, context, request):
407 # we need to explicitly return False here, so pyramid doesn't try to
406 # we need to explicitly return False here, so pyramid doesn't try to
408 # execute our view directly. We need our main handler to execute things
407 # execute our view directly. We need our main handler to execute things
409 return getattr(request, 'rpc_method') == self.method
408 return getattr(request, 'rpc_method') == self.method
410
409
411
410
412 def add_jsonrpc_method(config, view, **kwargs):
411 def add_jsonrpc_method(config, view, **kwargs):
413 # pop the method name
412 # pop the method name
414 method = kwargs.pop('method', None)
413 method = kwargs.pop('method', None)
415
414
416 if method is None:
415 if method is None:
417 raise ConfigurationError(
416 raise ConfigurationError(
418 'Cannot register a JSON-RPC method without specifying the '
417 'Cannot register a JSON-RPC method without specifying the '
419 '"method"')
418 '"method"')
420
419
421 # we define custom predicate, to enable to detect conflicting methods,
420 # we define custom predicate, to enable to detect conflicting methods,
422 # those predicates are kind of "translation" from the decorator variables
421 # those predicates are kind of "translation" from the decorator variables
423 # to internal predicates names
422 # to internal predicates names
424
423
425 kwargs['jsonrpc_method'] = method
424 kwargs['jsonrpc_method'] = method
426
425
427 # register our view into global view store for validation
426 # register our view into global view store for validation
428 config.registry.jsonrpc_methods[method] = view
427 config.registry.jsonrpc_methods[method] = view
429
428
430 # we're using our main request_view handler, here, so each method
429 # we're using our main request_view handler, here, so each method
431 # has a unified handler for itself
430 # has a unified handler for itself
432 config.add_view(request_view, route_name='apiv2', **kwargs)
431 config.add_view(request_view, route_name='apiv2', **kwargs)
433
432
434
433
435 class jsonrpc_method(object):
434 class jsonrpc_method(object):
436 """
435 """
437 decorator that works similar to @add_view_config decorator,
436 decorator that works similar to @add_view_config decorator,
438 but tailored for our JSON RPC
437 but tailored for our JSON RPC
439 """
438 """
440
439
441 venusian = venusian # for testing injection
440 venusian = venusian # for testing injection
442
441
443 def __init__(self, method=None, **kwargs):
442 def __init__(self, method=None, **kwargs):
444 self.method = method
443 self.method = method
445 self.kwargs = kwargs
444 self.kwargs = kwargs
446
445
447 def __call__(self, wrapped):
446 def __call__(self, wrapped):
448 kwargs = self.kwargs.copy()
447 kwargs = self.kwargs.copy()
449 kwargs['method'] = self.method or wrapped.__name__
448 kwargs['method'] = self.method or wrapped.__name__
450 depth = kwargs.pop('_depth', 0)
449 depth = kwargs.pop('_depth', 0)
451
450
452 def callback(context, name, ob):
451 def callback(context, name, ob):
453 config = context.config.with_package(info.module)
452 config = context.config.with_package(info.module)
454 config.add_jsonrpc_method(view=ob, **kwargs)
453 config.add_jsonrpc_method(view=ob, **kwargs)
455
454
456 info = venusian.attach(wrapped, callback, category='pyramid',
455 info = venusian.attach(wrapped, callback, category='pyramid',
457 depth=depth + 1)
456 depth=depth + 1)
458 if info.scope == 'class':
457 if info.scope == 'class':
459 # ensure that attr is set if decorating a class method
458 # ensure that attr is set if decorating a class method
460 kwargs.setdefault('attr', wrapped.__name__)
459 kwargs.setdefault('attr', wrapped.__name__)
461
460
462 kwargs['_info'] = info.codeinfo # fbo action_method
461 kwargs['_info'] = info.codeinfo # fbo action_method
463 return wrapped
462 return wrapped
464
463
465
464
466 class jsonrpc_deprecated_method(object):
465 class jsonrpc_deprecated_method(object):
467 """
466 """
468 Marks method as deprecated, adds log.warning, and inject special key to
467 Marks method as deprecated, adds log.warning, and inject special key to
469 the request variable to mark method as deprecated.
468 the request variable to mark method as deprecated.
470 Also injects special docstring that extract_docs will catch to mark
469 Also injects special docstring that extract_docs will catch to mark
471 method as deprecated.
470 method as deprecated.
472
471
473 :param use_method: specify which method should be used instead of
472 :param use_method: specify which method should be used instead of
474 the decorated one
473 the decorated one
475
474
476 Use like::
475 Use like::
477
476
478 @jsonrpc_method()
477 @jsonrpc_method()
479 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
480 def old_func(request, apiuser, arg1, arg2):
479 def old_func(request, apiuser, arg1, arg2):
481 ...
480 ...
482 """
481 """
483
482
484 def __init__(self, use_method, deprecated_at_version):
483 def __init__(self, use_method, deprecated_at_version):
485 self.use_method = use_method
484 self.use_method = use_method
486 self.deprecated_at_version = deprecated_at_version
485 self.deprecated_at_version = deprecated_at_version
487 self.deprecated_msg = ''
486 self.deprecated_msg = ''
488
487
489 def __call__(self, func):
488 def __call__(self, func):
490 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
491 method=self.use_method)
490 method=self.use_method)
492
491
493 docstring = """\n
492 docstring = """\n
494 .. deprecated:: {version}
493 .. deprecated:: {version}
495
494
496 {deprecation_message}
495 {deprecation_message}
497
496
498 {original_docstring}
497 {original_docstring}
499 """
498 """
500 func.__doc__ = docstring.format(
499 func.__doc__ = docstring.format(
501 version=self.deprecated_at_version,
500 version=self.deprecated_at_version,
502 deprecation_message=self.deprecated_msg,
501 deprecation_message=self.deprecated_msg,
503 original_docstring=func.__doc__)
502 original_docstring=func.__doc__)
504 return decorator.decorator(self.__wrapper, func)
503 return decorator.decorator(self.__wrapper, func)
505
504
506 def __wrapper(self, func, *fargs, **fkwargs):
505 def __wrapper(self, func, *fargs, **fkwargs):
507 log.warning('DEPRECATED API CALL on function %s, please '
506 log.warning('DEPRECATED API CALL on function %s, please '
508 'use `%s` instead', func, self.use_method)
507 'use `%s` instead', func, self.use_method)
509 # alter function docstring to mark as deprecated, this is picked up
508 # alter function docstring to mark as deprecated, this is picked up
510 # via fabric file that generates API DOC.
509 # via fabric file that generates API DOC.
511 result = func(*fargs, **fkwargs)
510 result = func(*fargs, **fkwargs)
512
511
513 request = fargs[0]
512 request = fargs[0]
514 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
515 return result
514 return result
516
515
517
516
518 def includeme(config):
517 def includeme(config):
519 plugin_module = 'rhodecode.api'
518 plugin_module = 'rhodecode.api'
520 plugin_settings = get_plugin_settings(
519 plugin_settings = get_plugin_settings(
521 plugin_module, config.registry.settings)
520 plugin_module, config.registry.settings)
522
521
523 if not hasattr(config.registry, 'jsonrpc_methods'):
522 if not hasattr(config.registry, 'jsonrpc_methods'):
524 config.registry.jsonrpc_methods = OrderedDict()
523 config.registry.jsonrpc_methods = OrderedDict()
525
524
526 # match filter by given method only
525 # match filter by given method only
527 config.add_view_predicate('jsonrpc_method', MethodPredicate)
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
528
527
529 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
530 serializer=json.dumps, indent=4))
529 serializer=json.dumps, indent=4))
531 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
532
531
533 config.add_route_predicate(
532 config.add_route_predicate(
534 'jsonrpc_call', RoutePredicate)
533 'jsonrpc_call', RoutePredicate)
535
534
536 config.add_route(
535 config.add_route(
537 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
538
537
539 config.scan(plugin_module, ignore='rhodecode.api.tests')
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
540 # register some exception handling view
539 # register some exception handling view
541 config.add_view(exception_view, context=JSONRPCBaseError)
540 config.add_view(exception_view, context=JSONRPCBaseError)
542 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
543 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,413 +1,413 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-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 time
21 import time
22 import logging
22 import logging
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.utils2 import time_to_datetime
36 from rhodecode.lib.utils2 import time_to_datetime
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
39 from rhodecode.model.gist import GistModel
39 from rhodecode.model.gist import GistModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.db import Gist, User, or_
41 from rhodecode.model.db import Gist, User, or_
42 from rhodecode.model import validation_schema
42 from rhodecode.model import validation_schema
43 from rhodecode.model.validation_schema.schemas import gist_schema
43 from rhodecode.model.validation_schema.schemas import gist_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class GistView(BaseAppView):
49 class GistView(BaseAppView):
50
50
51 def load_default_context(self):
51 def load_default_context(self):
52 _ = self.request.translate
52 _ = self.request.translate
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54 c.user = c.auth_user.get_instance()
54 c.user = c.auth_user.get_instance()
55
55
56 c.lifetime_values = [
56 c.lifetime_values = [
57 (-1, _('forever')),
57 (-1, _('forever')),
58 (5, _('5 minutes')),
58 (5, _('5 minutes')),
59 (60, _('1 hour')),
59 (60, _('1 hour')),
60 (60 * 24, _('1 day')),
60 (60 * 24, _('1 day')),
61 (60 * 24 * 30, _('1 month')),
61 (60 * 24 * 30, _('1 month')),
62 ]
62 ]
63
63
64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
65 c.acl_options = [
65 c.acl_options = [
66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
68 ]
68 ]
69
69
70
70
71 return c
71 return c
72
72
73 @LoginRequired()
73 @LoginRequired()
74 @view_config(
74 @view_config(
75 route_name='gists_show', request_method='GET',
75 route_name='gists_show', request_method='GET',
76 renderer='rhodecode:templates/admin/gists/index.mako')
76 renderer='rhodecode:templates/admin/gists/index.mako')
77 def gist_show_all(self):
77 def gist_show_all(self):
78 c = self.load_default_context()
78 c = self.load_default_context()
79
79
80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
81 c.show_private = self.request.GET.get('private') and not_default_user
81 c.show_private = self.request.GET.get('private') and not_default_user
82 c.show_public = self.request.GET.get('public') and not_default_user
82 c.show_public = self.request.GET.get('public') and not_default_user
83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
84
84
85 gists = _gists = Gist().query()\
85 gists = _gists = Gist().query()\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 .order_by(Gist.created_on.desc())
87 .order_by(Gist.created_on.desc())
88
88
89 c.active = 'public'
89 c.active = 'public'
90 # MY private
90 # MY private
91 if c.show_private and not c.show_public:
91 if c.show_private and not c.show_public:
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 c.active = 'my_private'
94 c.active = 'my_private'
95 # MY public
95 # MY public
96 elif c.show_public and not c.show_private:
96 elif c.show_public and not c.show_private:
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 c.active = 'my_public'
99 c.active = 'my_public'
100 # MY public+private
100 # MY public+private
101 elif c.show_private and c.show_public:
101 elif c.show_private and c.show_public:
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 Gist.gist_type == Gist.GIST_PRIVATE))\
103 Gist.gist_type == Gist.GIST_PRIVATE))\
104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
105 c.active = 'my_all'
105 c.active = 'my_all'
106 # Show all by super-admin
106 # Show all by super-admin
107 elif c.show_all:
107 elif c.show_all:
108 c.active = 'all'
108 c.active = 'all'
109 gists = _gists
109 gists = _gists
110
110
111 # default show ALL public gists
111 # default show ALL public gists
112 if not c.show_public and not c.show_private and not c.show_all:
112 if not c.show_public and not c.show_private and not c.show_all:
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 c.active = 'public'
114 c.active = 'public'
115
115
116 _render = self.request.get_partial_renderer(
116 _render = self.request.get_partial_renderer(
117 'rhodecode:templates/data_table/_dt_elements.mako')
117 'rhodecode:templates/data_table/_dt_elements.mako')
118
118
119 data = []
119 data = []
120
120
121 for gist in gists:
121 for gist in gists:
122 data.append({
122 data.append({
123 'created_on': _render('gist_created', gist.created_on),
123 'created_on': _render('gist_created', gist.created_on),
124 'created_on_raw': gist.created_on,
124 'created_on_raw': gist.created_on,
125 'type': _render('gist_type', gist.gist_type),
125 'type': _render('gist_type', gist.gist_type),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 'author_raw': h.escape(gist.owner.full_contact),
128 'author_raw': h.escape(gist.owner.full_contact),
129 'expires': _render('gist_expires', gist.gist_expires),
129 'expires': _render('gist_expires', gist.gist_expires),
130 'description': _render('gist_description', gist.gist_description)
130 'description': _render('gist_description', gist.gist_description)
131 })
131 })
132 c.data = json.dumps(data)
132 c.data = json.dumps(data)
133
133
134 return self._get_template_context(c)
134 return self._get_template_context(c)
135
135
136 @LoginRequired()
136 @LoginRequired()
137 @NotAnonymous()
137 @NotAnonymous()
138 @view_config(
138 @view_config(
139 route_name='gists_new', request_method='GET',
139 route_name='gists_new', request_method='GET',
140 renderer='rhodecode:templates/admin/gists/new.mako')
140 renderer='rhodecode:templates/admin/gists/new.mako')
141 def gist_new(self):
141 def gist_new(self):
142 c = self.load_default_context()
142 c = self.load_default_context()
143 return self._get_template_context(c)
143 return self._get_template_context(c)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @CSRFRequired()
147 @CSRFRequired()
148 @view_config(
148 @view_config(
149 route_name='gists_create', request_method='POST',
149 route_name='gists_create', request_method='POST',
150 renderer='rhodecode:templates/admin/gists/new.mako')
150 renderer='rhodecode:templates/admin/gists/new.mako')
151 def gist_create(self):
151 def gist_create(self):
152 _ = self.request.translate
152 _ = self.request.translate
153 c = self.load_default_context()
153 c = self.load_default_context()
154
154
155 data = dict(self.request.POST)
155 data = dict(self.request.POST)
156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
157 data['nodes'] = [{
157 data['nodes'] = [{
158 'filename': data['filename'],
158 'filename': data['filename'],
159 'content': data.get('content'),
159 'content': data.get('content'),
160 'mimetype': data.get('mimetype') # None is autodetect
160 'mimetype': data.get('mimetype') # None is autodetect
161 }]
161 }]
162
162
163 data['gist_type'] = (
163 data['gist_type'] = (
164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
165 data['gist_acl_level'] = (
165 data['gist_acl_level'] = (
166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
167
167
168 schema = gist_schema.GistSchema().bind(
168 schema = gist_schema.GistSchema().bind(
169 lifetime_options=[x[0] for x in c.lifetime_values])
169 lifetime_options=[x[0] for x in c.lifetime_values])
170
170
171 try:
171 try:
172
172
173 schema_data = schema.deserialize(data)
173 schema_data = schema.deserialize(data)
174 # convert to safer format with just KEYs so we sure no duplicates
174 # convert to safer format with just KEYs so we sure no duplicates
175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
176 schema_data['nodes'])
176 schema_data['nodes'])
177
177
178 gist = GistModel().create(
178 gist = GistModel().create(
179 gist_id=schema_data['gistid'], # custom access id not real ID
179 gist_id=schema_data['gistid'], # custom access id not real ID
180 description=schema_data['description'],
180 description=schema_data['description'],
181 owner=self._rhodecode_user.user_id,
181 owner=self._rhodecode_user.user_id,
182 gist_mapping=schema_data['nodes'],
182 gist_mapping=schema_data['nodes'],
183 gist_type=schema_data['gist_type'],
183 gist_type=schema_data['gist_type'],
184 lifetime=schema_data['lifetime'],
184 lifetime=schema_data['lifetime'],
185 gist_acl_level=schema_data['gist_acl_level']
185 gist_acl_level=schema_data['gist_acl_level']
186 )
186 )
187 Session().commit()
187 Session().commit()
188 new_gist_id = gist.gist_access_id
188 new_gist_id = gist.gist_access_id
189 except validation_schema.Invalid as errors:
189 except validation_schema.Invalid as errors:
190 defaults = data
190 defaults = data
191 errors = errors.asdict()
191 errors = errors.asdict()
192
192
193 if 'nodes.0.content' in errors:
193 if 'nodes.0.content' in errors:
194 errors['content'] = errors['nodes.0.content']
194 errors['content'] = errors['nodes.0.content']
195 del errors['nodes.0.content']
195 del errors['nodes.0.content']
196 if 'nodes.0.filename' in errors:
196 if 'nodes.0.filename' in errors:
197 errors['filename'] = errors['nodes.0.filename']
197 errors['filename'] = errors['nodes.0.filename']
198 del errors['nodes.0.filename']
198 del errors['nodes.0.filename']
199
199
200 data = render('rhodecode:templates/admin/gists/new.mako',
200 data = render('rhodecode:templates/admin/gists/new.mako',
201 self._get_template_context(c), self.request)
201 self._get_template_context(c), self.request)
202 html = formencode.htmlfill.render(
202 html = formencode.htmlfill.render(
203 data,
203 data,
204 defaults=defaults,
204 defaults=defaults,
205 errors=errors,
205 errors=errors,
206 prefix_error=False,
206 prefix_error=False,
207 encoding="UTF-8",
207 encoding="UTF-8",
208 force_defaults=False
208 force_defaults=False
209 )
209 )
210 return Response(html)
210 return Response(html)
211
211
212 except Exception:
212 except Exception:
213 log.exception("Exception while trying to create a gist")
213 log.exception("Exception while trying to create a gist")
214 h.flash(_('Error occurred during gist creation'), category='error')
214 h.flash(_('Error occurred during gist creation'), category='error')
215 raise HTTPFound(h.route_url('gists_new'))
215 raise HTTPFound(h.route_url('gists_new'))
216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
217
217
218 @LoginRequired()
218 @LoginRequired()
219 @NotAnonymous()
219 @NotAnonymous()
220 @CSRFRequired()
220 @CSRFRequired()
221 @view_config(
221 @view_config(
222 route_name='gist_delete', request_method='POST')
222 route_name='gist_delete', request_method='POST')
223 def gist_delete(self):
223 def gist_delete(self):
224 _ = self.request.translate
224 _ = self.request.translate
225 gist_id = self.request.matchdict['gist_id']
225 gist_id = self.request.matchdict['gist_id']
226
226
227 c = self.load_default_context()
227 c = self.load_default_context()
228 c.gist = Gist.get_or_404(gist_id)
228 c.gist = Gist.get_or_404(gist_id)
229
229
230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
231 if not (h.HasPermissionAny('hg.admin')() or owner):
231 if not (h.HasPermissionAny('hg.admin')() or owner):
232 log.warning('Deletion of Gist was forbidden '
232 log.warning('Deletion of Gist was forbidden '
233 'by unauthorized user: `%s`', self._rhodecode_user)
233 'by unauthorized user: `%s`', self._rhodecode_user)
234 raise HTTPNotFound()
234 raise HTTPNotFound()
235
235
236 GistModel().delete(c.gist)
236 GistModel().delete(c.gist)
237 Session().commit()
237 Session().commit()
238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
239
239
240 raise HTTPFound(h.route_url('gists_show'))
240 raise HTTPFound(h.route_url('gists_show'))
241
241
242 def _get_gist(self, gist_id):
242 def _get_gist(self, gist_id):
243
243
244 gist = Gist.get_or_404(gist_id)
244 gist = Gist.get_or_404(gist_id)
245
245
246 # Check if this gist is expired
246 # Check if this gist is expired
247 if gist.gist_expires != -1:
247 if gist.gist_expires != -1:
248 if time.time() > gist.gist_expires:
248 if time.time() > gist.gist_expires:
249 log.error(
249 log.error(
250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
251 raise HTTPNotFound()
251 raise HTTPNotFound()
252
252
253 # check if this gist requires a login
253 # check if this gist requires a login
254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
256 log.error("Anonymous user %s tried to access protected gist `%s`",
256 log.error("Anonymous user %s tried to access protected gist `%s`",
257 self._rhodecode_user, gist_id)
257 self._rhodecode_user, gist_id)
258 raise HTTPNotFound()
258 raise HTTPNotFound()
259 return gist
259 return gist
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @view_config(
262 @view_config(
263 route_name='gist_show', request_method='GET',
263 route_name='gist_show', request_method='GET',
264 renderer='rhodecode:templates/admin/gists/show.mako')
264 renderer='rhodecode:templates/admin/gists/show.mako')
265 @view_config(
265 @view_config(
266 route_name='gist_show_rev', request_method='GET',
266 route_name='gist_show_rev', request_method='GET',
267 renderer='rhodecode:templates/admin/gists/show.mako')
267 renderer='rhodecode:templates/admin/gists/show.mako')
268 @view_config(
268 @view_config(
269 route_name='gist_show_formatted', request_method='GET',
269 route_name='gist_show_formatted', request_method='GET',
270 renderer=None)
270 renderer=None)
271 @view_config(
271 @view_config(
272 route_name='gist_show_formatted_path', request_method='GET',
272 route_name='gist_show_formatted_path', request_method='GET',
273 renderer=None)
273 renderer=None)
274 def gist_show(self):
274 def gist_show(self):
275 gist_id = self.request.matchdict['gist_id']
275 gist_id = self.request.matchdict['gist_id']
276
276
277 # TODO(marcink): expose those via matching dict
277 # TODO(marcink): expose those via matching dict
278 revision = self.request.matchdict.get('revision', 'tip')
278 revision = self.request.matchdict.get('revision', 'tip')
279 f_path = self.request.matchdict.get('f_path', None)
279 f_path = self.request.matchdict.get('f_path', None)
280 return_format = self.request.matchdict.get('format')
280 return_format = self.request.matchdict.get('format')
281
281
282 c = self.load_default_context()
282 c = self.load_default_context()
283 c.gist = self._get_gist(gist_id)
283 c.gist = self._get_gist(gist_id)
284 c.render = not self.request.GET.get('no-render', False)
284 c.render = not self.request.GET.get('no-render', False)
285
285
286 try:
286 try:
287 c.file_last_commit, c.files = GistModel().get_gist_files(
287 c.file_last_commit, c.files = GistModel().get_gist_files(
288 gist_id, revision=revision)
288 gist_id, revision=revision)
289 except VCSError:
289 except VCSError:
290 log.exception("Exception in gist show")
290 log.exception("Exception in gist show")
291 raise HTTPNotFound()
291 raise HTTPNotFound()
292
292
293 if return_format == 'raw':
293 if return_format == 'raw':
294 content = '\n\n'.join([f.content for f in c.files
294 content = '\n\n'.join([f.content for f in c.files
295 if (f_path is None or f.path == f_path)])
295 if (f_path is None or f.path == f_path)])
296 response = Response(content)
296 response = Response(content)
297 response.content_type = 'text/plain'
297 response.content_type = 'text/plain'
298 return response
298 return response
299
299
300 return self._get_template_context(c)
300 return self._get_template_context(c)
301
301
302 @LoginRequired()
302 @LoginRequired()
303 @NotAnonymous()
303 @NotAnonymous()
304 @view_config(
304 @view_config(
305 route_name='gist_edit', request_method='GET',
305 route_name='gist_edit', request_method='GET',
306 renderer='rhodecode:templates/admin/gists/edit.mako')
306 renderer='rhodecode:templates/admin/gists/edit.mako')
307 def gist_edit(self):
307 def gist_edit(self):
308 _ = self.request.translate
308 _ = self.request.translate
309 gist_id = self.request.matchdict['gist_id']
309 gist_id = self.request.matchdict['gist_id']
310 c = self.load_default_context()
310 c = self.load_default_context()
311 c.gist = self._get_gist(gist_id)
311 c.gist = self._get_gist(gist_id)
312
312
313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
314 if not (h.HasPermissionAny('hg.admin')() or owner):
314 if not (h.HasPermissionAny('hg.admin')() or owner):
315 raise HTTPNotFound()
315 raise HTTPNotFound()
316
316
317 try:
317 try:
318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
319 except VCSError:
319 except VCSError:
320 log.exception("Exception in gist edit")
320 log.exception("Exception in gist edit")
321 raise HTTPNotFound()
321 raise HTTPNotFound()
322
322
323 if c.gist.gist_expires == -1:
323 if c.gist.gist_expires == -1:
324 expiry = _('never')
324 expiry = _('never')
325 else:
325 else:
326 # this cannot use timeago, since it's used in select2 as a value
326 # this cannot use timeago, since it's used in select2 as a value
327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
328
328
329 c.lifetime_values.append(
329 c.lifetime_values.append(
330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
331 )
331 )
332
332
333 return self._get_template_context(c)
333 return self._get_template_context(c)
334
334
335 @LoginRequired()
335 @LoginRequired()
336 @NotAnonymous()
336 @NotAnonymous()
337 @CSRFRequired()
337 @CSRFRequired()
338 @view_config(
338 @view_config(
339 route_name='gist_update', request_method='POST',
339 route_name='gist_update', request_method='POST',
340 renderer='rhodecode:templates/admin/gists/edit.mako')
340 renderer='rhodecode:templates/admin/gists/edit.mako')
341 def gist_update(self):
341 def gist_update(self):
342 _ = self.request.translate
342 _ = self.request.translate
343 gist_id = self.request.matchdict['gist_id']
343 gist_id = self.request.matchdict['gist_id']
344 c = self.load_default_context()
344 c = self.load_default_context()
345 c.gist = self._get_gist(gist_id)
345 c.gist = self._get_gist(gist_id)
346
346
347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
348 if not (h.HasPermissionAny('hg.admin')() or owner):
348 if not (h.HasPermissionAny('hg.admin')() or owner):
349 raise HTTPNotFound()
349 raise HTTPNotFound()
350
350
351 data = peppercorn.parse(self.request.POST.items())
351 data = peppercorn.parse(self.request.POST.items())
352
352
353 schema = gist_schema.GistSchema()
353 schema = gist_schema.GistSchema()
354 schema = schema.bind(
354 schema = schema.bind(
355 # '0' is special value to leave lifetime untouched
355 # '0' is special value to leave lifetime untouched
356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
357 )
357 )
358
358
359 try:
359 try:
360 schema_data = schema.deserialize(data)
360 schema_data = schema.deserialize(data)
361 # convert to safer format with just KEYs so we sure no duplicates
361 # convert to safer format with just KEYs so we sure no duplicates
362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
363 schema_data['nodes'])
363 schema_data['nodes'])
364
364
365 GistModel().update(
365 GistModel().update(
366 gist=c.gist,
366 gist=c.gist,
367 description=schema_data['description'],
367 description=schema_data['description'],
368 owner=c.gist.owner,
368 owner=c.gist.owner,
369 gist_mapping=schema_data['nodes'],
369 gist_mapping=schema_data['nodes'],
370 lifetime=schema_data['lifetime'],
370 lifetime=schema_data['lifetime'],
371 gist_acl_level=schema_data['gist_acl_level']
371 gist_acl_level=schema_data['gist_acl_level']
372 )
372 )
373
373
374 Session().commit()
374 Session().commit()
375 h.flash(_('Successfully updated gist content'), category='success')
375 h.flash(_('Successfully updated gist content'), category='success')
376 except NodeNotChangedError:
376 except NodeNotChangedError:
377 # raised if nothing was changed in repo itself. We anyway then
377 # raised if nothing was changed in repo itself. We anyway then
378 # store only DB stuff for gist
378 # store only DB stuff for gist
379 Session().commit()
379 Session().commit()
380 h.flash(_('Successfully updated gist data'), category='success')
380 h.flash(_('Successfully updated gist data'), category='success')
381 except validation_schema.Invalid as errors:
381 except validation_schema.Invalid as errors:
382 errors = h.escape(errors.asdict())
382 errors = h.escape(errors.asdict())
383 h.flash(_('Error occurred during update of gist {}: {}').format(
383 h.flash(_('Error occurred during update of gist {}: {}').format(
384 gist_id, errors), category='error')
384 gist_id, errors), category='error')
385 except Exception:
385 except Exception:
386 log.exception("Exception in gist edit")
386 log.exception("Exception in gist edit")
387 h.flash(_('Error occurred during update of gist %s') % gist_id,
387 h.flash(_('Error occurred during update of gist %s') % gist_id,
388 category='error')
388 category='error')
389
389
390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
391
391
392 @LoginRequired()
392 @LoginRequired()
393 @NotAnonymous()
393 @NotAnonymous()
394 @view_config(
394 @view_config(
395 route_name='gist_edit_check_revision', request_method='GET',
395 route_name='gist_edit_check_revision', request_method='GET',
396 renderer='json_ext')
396 renderer='json_ext')
397 def gist_edit_check_revision(self):
397 def gist_edit_check_revision(self):
398 _ = self.request.translate
398 _ = self.request.translate
399 gist_id = self.request.matchdict['gist_id']
399 gist_id = self.request.matchdict['gist_id']
400 c = self.load_default_context()
400 c = self.load_default_context()
401 c.gist = self._get_gist(gist_id)
401 c.gist = self._get_gist(gist_id)
402
402
403 last_rev = c.gist.scm_instance().get_commit()
403 last_rev = c.gist.scm_instance().get_commit()
404 success = True
404 success = True
405 revision = self.request.GET.get('revision')
405 revision = self.request.GET.get('revision')
406
406
407 if revision != last_rev.raw_id:
407 if revision != last_rev.raw_id:
408 log.error('Last revision %s is different then submitted %s'
408 log.error('Last revision %s is different then submitted %s',
409 % (revision, last_rev))
409 revision, last_rev)
410 # our gist has newer version than we
410 # our gist has newer version than we
411 success = False
411 success = False
412
412
413 return {'success': success}
413 return {'success': success}
@@ -1,461 +1,461 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-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 time
21 import time
22 import collections
22 import collections
23 import datetime
23 import datetime
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28 import requests
28 import requests
29
29
30 from pyramid.httpexceptions import HTTPFound
30 from pyramid.httpexceptions import HTTPFound
31 from pyramid.view import view_config
31 from pyramid.view import view_config
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 from rhodecode.events import UserRegistered, trigger
35 from rhodecode.events import UserRegistered, trigger
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
40 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.base import get_ip_addr
41 from rhodecode.lib.exceptions import UserCreationError
41 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
45 from rhodecode.model.meta import Session
45 from rhodecode.model.meta import Session
46 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.auth_token import AuthTokenModel
47 from rhodecode.model.settings import SettingsModel
47 from rhodecode.model.settings import SettingsModel
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.translation import _
49 from rhodecode.translation import _
50
50
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54 CaptchaData = collections.namedtuple(
54 CaptchaData = collections.namedtuple(
55 'CaptchaData', 'active, private_key, public_key')
55 'CaptchaData', 'active, private_key, public_key')
56
56
57
57
58 def _store_user_in_session(session, username, remember=False):
58 def _store_user_in_session(session, username, remember=False):
59 user = User.get_by_username(username, case_insensitive=True)
59 user = User.get_by_username(username, case_insensitive=True)
60 auth_user = AuthUser(user.user_id)
60 auth_user = AuthUser(user.user_id)
61 auth_user.set_authenticated()
61 auth_user.set_authenticated()
62 cs = auth_user.get_cookie_store()
62 cs = auth_user.get_cookie_store()
63 session['rhodecode_user'] = cs
63 session['rhodecode_user'] = cs
64 user.update_lastlogin()
64 user.update_lastlogin()
65 Session().commit()
65 Session().commit()
66
66
67 # If they want to be remembered, update the cookie
67 # If they want to be remembered, update the cookie
68 if remember:
68 if remember:
69 _year = (datetime.datetime.now() +
69 _year = (datetime.datetime.now() +
70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
71 session._set_cookie_expires(_year)
71 session._set_cookie_expires(_year)
72
72
73 session.save()
73 session.save()
74
74
75 safe_cs = cs.copy()
75 safe_cs = cs.copy()
76 safe_cs['password'] = '****'
76 safe_cs['password'] = '****'
77 log.info('user %s is now authenticated and stored in '
77 log.info('user %s is now authenticated and stored in '
78 'session, session attrs %s', username, safe_cs)
78 'session, session attrs %s', username, safe_cs)
79
79
80 # dumps session attrs back to cookie
80 # dumps session attrs back to cookie
81 session._update_cookie_out()
81 session._update_cookie_out()
82 # we set new cookie
82 # we set new cookie
83 headers = None
83 headers = None
84 if session.request['set_cookie']:
84 if session.request['set_cookie']:
85 # send set-cookie headers back to response to update cookie
85 # send set-cookie headers back to response to update cookie
86 headers = [('Set-Cookie', session.request['cookie_out'])]
86 headers = [('Set-Cookie', session.request['cookie_out'])]
87 return headers
87 return headers
88
88
89
89
90 def get_came_from(request):
90 def get_came_from(request):
91 came_from = safe_str(request.GET.get('came_from', ''))
91 came_from = safe_str(request.GET.get('came_from', ''))
92 parsed = urlparse.urlparse(came_from)
92 parsed = urlparse.urlparse(came_from)
93 allowed_schemes = ['http', 'https']
93 allowed_schemes = ['http', 'https']
94 default_came_from = h.route_path('home')
94 default_came_from = h.route_path('home')
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 log.error('Suspicious URL scheme detected %s for url %s' %
96 log.error('Suspicious URL scheme detected %s for url %s',
97 (parsed.scheme, parsed))
97 parsed.scheme, parsed)
98 came_from = default_came_from
98 came_from = default_came_from
99 elif parsed.netloc and request.host != parsed.netloc:
99 elif parsed.netloc and request.host != parsed.netloc:
100 log.error('Suspicious NETLOC detected %s for url %s server url '
100 log.error('Suspicious NETLOC detected %s for url %s server url '
101 'is: %s' % (parsed.netloc, parsed, request.host))
101 'is: %s', parsed.netloc, parsed, request.host)
102 came_from = default_came_from
102 came_from = default_came_from
103 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
104 log.error('Header injection detected `%s` for url %s server url ' %
104 log.error('Header injection detected `%s` for url %s server url ',
105 (parsed.path, parsed))
105 parsed.path, parsed)
106 came_from = default_came_from
106 came_from = default_came_from
107
107
108 return came_from or default_came_from
108 return came_from or default_came_from
109
109
110
110
111 class LoginView(BaseAppView):
111 class LoginView(BaseAppView):
112
112
113 def load_default_context(self):
113 def load_default_context(self):
114 c = self._get_local_tmpl_context()
114 c = self._get_local_tmpl_context()
115 c.came_from = get_came_from(self.request)
115 c.came_from = get_came_from(self.request)
116
116
117 return c
117 return c
118
118
119 def _get_captcha_data(self):
119 def _get_captcha_data(self):
120 settings = SettingsModel().get_all_settings()
120 settings = SettingsModel().get_all_settings()
121 private_key = settings.get('rhodecode_captcha_private_key')
121 private_key = settings.get('rhodecode_captcha_private_key')
122 public_key = settings.get('rhodecode_captcha_public_key')
122 public_key = settings.get('rhodecode_captcha_public_key')
123 active = bool(private_key)
123 active = bool(private_key)
124 return CaptchaData(
124 return CaptchaData(
125 active=active, private_key=private_key, public_key=public_key)
125 active=active, private_key=private_key, public_key=public_key)
126
126
127 def validate_captcha(self, private_key):
127 def validate_captcha(self, private_key):
128
128
129 captcha_rs = self.request.POST.get('g-recaptcha-response')
129 captcha_rs = self.request.POST.get('g-recaptcha-response')
130 url = "https://www.google.com/recaptcha/api/siteverify"
130 url = "https://www.google.com/recaptcha/api/siteverify"
131 params = {
131 params = {
132 'secret': private_key,
132 'secret': private_key,
133 'response': captcha_rs,
133 'response': captcha_rs,
134 'remoteip': get_ip_addr(self.request.environ)
134 'remoteip': get_ip_addr(self.request.environ)
135 }
135 }
136 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
136 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
137 verify_rs = verify_rs.json()
137 verify_rs = verify_rs.json()
138 captcha_status = verify_rs.get('success', False)
138 captcha_status = verify_rs.get('success', False)
139 captcha_errors = verify_rs.get('error-codes', [])
139 captcha_errors = verify_rs.get('error-codes', [])
140 if not isinstance(captcha_errors, list):
140 if not isinstance(captcha_errors, list):
141 captcha_errors = [captcha_errors]
141 captcha_errors = [captcha_errors]
142 captcha_errors = ', '.join(captcha_errors)
142 captcha_errors = ', '.join(captcha_errors)
143 captcha_message = ''
143 captcha_message = ''
144 if captcha_status is False:
144 if captcha_status is False:
145 captcha_message = "Bad captcha. Errors: {}".format(
145 captcha_message = "Bad captcha. Errors: {}".format(
146 captcha_errors)
146 captcha_errors)
147
147
148 return captcha_status, captcha_message
148 return captcha_status, captcha_message
149
149
150 @view_config(
150 @view_config(
151 route_name='login', request_method='GET',
151 route_name='login', request_method='GET',
152 renderer='rhodecode:templates/login.mako')
152 renderer='rhodecode:templates/login.mako')
153 def login(self):
153 def login(self):
154 c = self.load_default_context()
154 c = self.load_default_context()
155 auth_user = self._rhodecode_user
155 auth_user = self._rhodecode_user
156
156
157 # redirect if already logged in
157 # redirect if already logged in
158 if (auth_user.is_authenticated and
158 if (auth_user.is_authenticated and
159 not auth_user.is_default and auth_user.ip_allowed):
159 not auth_user.is_default and auth_user.ip_allowed):
160 raise HTTPFound(c.came_from)
160 raise HTTPFound(c.came_from)
161
161
162 # check if we use headers plugin, and try to login using it.
162 # check if we use headers plugin, and try to login using it.
163 try:
163 try:
164 log.debug('Running PRE-AUTH for headers based authentication')
164 log.debug('Running PRE-AUTH for headers based authentication')
165 auth_info = authenticate(
165 auth_info = authenticate(
166 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
166 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
167 if auth_info:
167 if auth_info:
168 headers = _store_user_in_session(
168 headers = _store_user_in_session(
169 self.session, auth_info.get('username'))
169 self.session, auth_info.get('username'))
170 raise HTTPFound(c.came_from, headers=headers)
170 raise HTTPFound(c.came_from, headers=headers)
171 except UserCreationError as e:
171 except UserCreationError as e:
172 log.error(e)
172 log.error(e)
173 h.flash(e, category='error')
173 h.flash(e, category='error')
174
174
175 return self._get_template_context(c)
175 return self._get_template_context(c)
176
176
177 @view_config(
177 @view_config(
178 route_name='login', request_method='POST',
178 route_name='login', request_method='POST',
179 renderer='rhodecode:templates/login.mako')
179 renderer='rhodecode:templates/login.mako')
180 def login_post(self):
180 def login_post(self):
181 c = self.load_default_context()
181 c = self.load_default_context()
182
182
183 login_form = LoginForm(self.request.translate)()
183 login_form = LoginForm(self.request.translate)()
184
184
185 try:
185 try:
186 self.session.invalidate()
186 self.session.invalidate()
187 form_result = login_form.to_python(self.request.POST)
187 form_result = login_form.to_python(self.request.POST)
188 # form checks for username/password, now we're authenticated
188 # form checks for username/password, now we're authenticated
189 headers = _store_user_in_session(
189 headers = _store_user_in_session(
190 self.session,
190 self.session,
191 username=form_result['username'],
191 username=form_result['username'],
192 remember=form_result['remember'])
192 remember=form_result['remember'])
193 log.debug('Redirecting to "%s" after login.', c.came_from)
193 log.debug('Redirecting to "%s" after login.', c.came_from)
194
194
195 audit_user = audit_logger.UserWrap(
195 audit_user = audit_logger.UserWrap(
196 username=self.request.POST.get('username'),
196 username=self.request.POST.get('username'),
197 ip_addr=self.request.remote_addr)
197 ip_addr=self.request.remote_addr)
198 action_data = {'user_agent': self.request.user_agent}
198 action_data = {'user_agent': self.request.user_agent}
199 audit_logger.store_web(
199 audit_logger.store_web(
200 'user.login.success', action_data=action_data,
200 'user.login.success', action_data=action_data,
201 user=audit_user, commit=True)
201 user=audit_user, commit=True)
202
202
203 raise HTTPFound(c.came_from, headers=headers)
203 raise HTTPFound(c.came_from, headers=headers)
204 except formencode.Invalid as errors:
204 except formencode.Invalid as errors:
205 defaults = errors.value
205 defaults = errors.value
206 # remove password from filling in form again
206 # remove password from filling in form again
207 defaults.pop('password', None)
207 defaults.pop('password', None)
208 render_ctx = {
208 render_ctx = {
209 'errors': errors.error_dict,
209 'errors': errors.error_dict,
210 'defaults': defaults,
210 'defaults': defaults,
211 }
211 }
212
212
213 audit_user = audit_logger.UserWrap(
213 audit_user = audit_logger.UserWrap(
214 username=self.request.POST.get('username'),
214 username=self.request.POST.get('username'),
215 ip_addr=self.request.remote_addr)
215 ip_addr=self.request.remote_addr)
216 action_data = {'user_agent': self.request.user_agent}
216 action_data = {'user_agent': self.request.user_agent}
217 audit_logger.store_web(
217 audit_logger.store_web(
218 'user.login.failure', action_data=action_data,
218 'user.login.failure', action_data=action_data,
219 user=audit_user, commit=True)
219 user=audit_user, commit=True)
220 return self._get_template_context(c, **render_ctx)
220 return self._get_template_context(c, **render_ctx)
221
221
222 except UserCreationError as e:
222 except UserCreationError as e:
223 # headers auth or other auth functions that create users on
223 # headers auth or other auth functions that create users on
224 # the fly can throw this exception signaling that there's issue
224 # the fly can throw this exception signaling that there's issue
225 # with user creation, explanation should be provided in
225 # with user creation, explanation should be provided in
226 # Exception itself
226 # Exception itself
227 h.flash(e, category='error')
227 h.flash(e, category='error')
228 return self._get_template_context(c)
228 return self._get_template_context(c)
229
229
230 @CSRFRequired()
230 @CSRFRequired()
231 @view_config(route_name='logout', request_method='POST')
231 @view_config(route_name='logout', request_method='POST')
232 def logout(self):
232 def logout(self):
233 auth_user = self._rhodecode_user
233 auth_user = self._rhodecode_user
234 log.info('Deleting session for user: `%s`', auth_user)
234 log.info('Deleting session for user: `%s`', auth_user)
235
235
236 action_data = {'user_agent': self.request.user_agent}
236 action_data = {'user_agent': self.request.user_agent}
237 audit_logger.store_web(
237 audit_logger.store_web(
238 'user.logout', action_data=action_data,
238 'user.logout', action_data=action_data,
239 user=auth_user, commit=True)
239 user=auth_user, commit=True)
240 self.session.delete()
240 self.session.delete()
241 return HTTPFound(h.route_path('home'))
241 return HTTPFound(h.route_path('home'))
242
242
243 @HasPermissionAnyDecorator(
243 @HasPermissionAnyDecorator(
244 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
244 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
245 @view_config(
245 @view_config(
246 route_name='register', request_method='GET',
246 route_name='register', request_method='GET',
247 renderer='rhodecode:templates/register.mako',)
247 renderer='rhodecode:templates/register.mako',)
248 def register(self, defaults=None, errors=None):
248 def register(self, defaults=None, errors=None):
249 c = self.load_default_context()
249 c = self.load_default_context()
250 defaults = defaults or {}
250 defaults = defaults or {}
251 errors = errors or {}
251 errors = errors or {}
252
252
253 settings = SettingsModel().get_all_settings()
253 settings = SettingsModel().get_all_settings()
254 register_message = settings.get('rhodecode_register_message') or ''
254 register_message = settings.get('rhodecode_register_message') or ''
255 captcha = self._get_captcha_data()
255 captcha = self._get_captcha_data()
256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
257 .AuthUser().permissions['global']
257 .AuthUser().permissions['global']
258
258
259 render_ctx = self._get_template_context(c)
259 render_ctx = self._get_template_context(c)
260 render_ctx.update({
260 render_ctx.update({
261 'defaults': defaults,
261 'defaults': defaults,
262 'errors': errors,
262 'errors': errors,
263 'auto_active': auto_active,
263 'auto_active': auto_active,
264 'captcha_active': captcha.active,
264 'captcha_active': captcha.active,
265 'captcha_public_key': captcha.public_key,
265 'captcha_public_key': captcha.public_key,
266 'register_message': register_message,
266 'register_message': register_message,
267 })
267 })
268 return render_ctx
268 return render_ctx
269
269
270 @HasPermissionAnyDecorator(
270 @HasPermissionAnyDecorator(
271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
272 @view_config(
272 @view_config(
273 route_name='register', request_method='POST',
273 route_name='register', request_method='POST',
274 renderer='rhodecode:templates/register.mako')
274 renderer='rhodecode:templates/register.mako')
275 def register_post(self):
275 def register_post(self):
276 self.load_default_context()
276 self.load_default_context()
277 captcha = self._get_captcha_data()
277 captcha = self._get_captcha_data()
278 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
278 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
279 .AuthUser().permissions['global']
279 .AuthUser().permissions['global']
280
280
281 register_form = RegisterForm(self.request.translate)()
281 register_form = RegisterForm(self.request.translate)()
282 try:
282 try:
283
283
284 form_result = register_form.to_python(self.request.POST)
284 form_result = register_form.to_python(self.request.POST)
285 form_result['active'] = auto_active
285 form_result['active'] = auto_active
286
286
287 if captcha.active:
287 if captcha.active:
288 captcha_status, captcha_message = self.validate_captcha(
288 captcha_status, captcha_message = self.validate_captcha(
289 captcha.private_key)
289 captcha.private_key)
290
290
291 if not captcha_status:
291 if not captcha_status:
292 _value = form_result
292 _value = form_result
293 _msg = _('Bad captcha')
293 _msg = _('Bad captcha')
294 error_dict = {'recaptcha_field': captcha_message}
294 error_dict = {'recaptcha_field': captcha_message}
295 raise formencode.Invalid(
295 raise formencode.Invalid(
296 _msg, _value, None, error_dict=error_dict)
296 _msg, _value, None, error_dict=error_dict)
297
297
298 new_user = UserModel().create_registration(form_result)
298 new_user = UserModel().create_registration(form_result)
299
299
300 action_data = {'data': new_user.get_api_data(),
300 action_data = {'data': new_user.get_api_data(),
301 'user_agent': self.request.user_agent}
301 'user_agent': self.request.user_agent}
302
302
303 audit_user = audit_logger.UserWrap(
303 audit_user = audit_logger.UserWrap(
304 username=new_user.username,
304 username=new_user.username,
305 user_id=new_user.user_id,
305 user_id=new_user.user_id,
306 ip_addr=self.request.remote_addr)
306 ip_addr=self.request.remote_addr)
307
307
308 audit_logger.store_web(
308 audit_logger.store_web(
309 'user.register', action_data=action_data,
309 'user.register', action_data=action_data,
310 user=audit_user)
310 user=audit_user)
311
311
312 event = UserRegistered(user=new_user, session=self.session)
312 event = UserRegistered(user=new_user, session=self.session)
313 trigger(event)
313 trigger(event)
314 h.flash(
314 h.flash(
315 _('You have successfully registered with RhodeCode'),
315 _('You have successfully registered with RhodeCode'),
316 category='success')
316 category='success')
317 Session().commit()
317 Session().commit()
318
318
319 redirect_ro = self.request.route_path('login')
319 redirect_ro = self.request.route_path('login')
320 raise HTTPFound(redirect_ro)
320 raise HTTPFound(redirect_ro)
321
321
322 except formencode.Invalid as errors:
322 except formencode.Invalid as errors:
323 errors.value.pop('password', None)
323 errors.value.pop('password', None)
324 errors.value.pop('password_confirmation', None)
324 errors.value.pop('password_confirmation', None)
325 return self.register(
325 return self.register(
326 defaults=errors.value, errors=errors.error_dict)
326 defaults=errors.value, errors=errors.error_dict)
327
327
328 except UserCreationError as e:
328 except UserCreationError as e:
329 # container auth or other auth functions that create users on
329 # container auth or other auth functions that create users on
330 # the fly can throw this exception signaling that there's issue
330 # the fly can throw this exception signaling that there's issue
331 # with user creation, explanation should be provided in
331 # with user creation, explanation should be provided in
332 # Exception itself
332 # Exception itself
333 h.flash(e, category='error')
333 h.flash(e, category='error')
334 return self.register()
334 return self.register()
335
335
336 @view_config(
336 @view_config(
337 route_name='reset_password', request_method=('GET', 'POST'),
337 route_name='reset_password', request_method=('GET', 'POST'),
338 renderer='rhodecode:templates/password_reset.mako')
338 renderer='rhodecode:templates/password_reset.mako')
339 def password_reset(self):
339 def password_reset(self):
340 c = self.load_default_context()
340 c = self.load_default_context()
341 captcha = self._get_captcha_data()
341 captcha = self._get_captcha_data()
342
342
343 template_context = {
343 template_context = {
344 'captcha_active': captcha.active,
344 'captcha_active': captcha.active,
345 'captcha_public_key': captcha.public_key,
345 'captcha_public_key': captcha.public_key,
346 'defaults': {},
346 'defaults': {},
347 'errors': {},
347 'errors': {},
348 }
348 }
349
349
350 # always send implicit message to prevent from discovery of
350 # always send implicit message to prevent from discovery of
351 # matching emails
351 # matching emails
352 msg = _('If such email exists, a password reset link was sent to it.')
352 msg = _('If such email exists, a password reset link was sent to it.')
353
353
354 if self.request.POST:
354 if self.request.POST:
355 if h.HasPermissionAny('hg.password_reset.disabled')():
355 if h.HasPermissionAny('hg.password_reset.disabled')():
356 _email = self.request.POST.get('email', '')
356 _email = self.request.POST.get('email', '')
357 log.error('Failed attempt to reset password for `%s`.', _email)
357 log.error('Failed attempt to reset password for `%s`.', _email)
358 h.flash(_('Password reset has been disabled.'),
358 h.flash(_('Password reset has been disabled.'),
359 category='error')
359 category='error')
360 return HTTPFound(self.request.route_path('reset_password'))
360 return HTTPFound(self.request.route_path('reset_password'))
361
361
362 password_reset_form = PasswordResetForm(self.request.translate)()
362 password_reset_form = PasswordResetForm(self.request.translate)()
363 try:
363 try:
364 form_result = password_reset_form.to_python(
364 form_result = password_reset_form.to_python(
365 self.request.POST)
365 self.request.POST)
366 user_email = form_result['email']
366 user_email = form_result['email']
367
367
368 if captcha.active:
368 if captcha.active:
369 captcha_status, captcha_message = self.validate_captcha(
369 captcha_status, captcha_message = self.validate_captcha(
370 captcha.private_key)
370 captcha.private_key)
371
371
372 if not captcha_status:
372 if not captcha_status:
373 _value = form_result
373 _value = form_result
374 _msg = _('Bad captcha')
374 _msg = _('Bad captcha')
375 error_dict = {'recaptcha_field': captcha_message}
375 error_dict = {'recaptcha_field': captcha_message}
376 raise formencode.Invalid(
376 raise formencode.Invalid(
377 _msg, _value, None, error_dict=error_dict)
377 _msg, _value, None, error_dict=error_dict)
378
378
379 # Generate reset URL and send mail.
379 # Generate reset URL and send mail.
380 user = User.get_by_email(user_email)
380 user = User.get_by_email(user_email)
381
381
382 # generate password reset token that expires in 10 minutes
382 # generate password reset token that expires in 10 minutes
383 description = u'Generated token for password reset from {}'.format(
383 description = u'Generated token for password reset from {}'.format(
384 datetime.datetime.now().isoformat())
384 datetime.datetime.now().isoformat())
385
385
386 reset_token = UserModel().add_auth_token(
386 reset_token = UserModel().add_auth_token(
387 user=user, lifetime_minutes=10,
387 user=user, lifetime_minutes=10,
388 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
388 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
389 description=description)
389 description=description)
390 Session().commit()
390 Session().commit()
391
391
392 log.debug('Successfully created password recovery token')
392 log.debug('Successfully created password recovery token')
393 password_reset_url = self.request.route_url(
393 password_reset_url = self.request.route_url(
394 'reset_password_confirmation',
394 'reset_password_confirmation',
395 _query={'key': reset_token.api_key})
395 _query={'key': reset_token.api_key})
396 UserModel().reset_password_link(
396 UserModel().reset_password_link(
397 form_result, password_reset_url)
397 form_result, password_reset_url)
398 # Display success message and redirect.
398 # Display success message and redirect.
399 h.flash(msg, category='success')
399 h.flash(msg, category='success')
400
400
401 action_data = {'email': user_email,
401 action_data = {'email': user_email,
402 'user_agent': self.request.user_agent}
402 'user_agent': self.request.user_agent}
403 audit_logger.store_web(
403 audit_logger.store_web(
404 'user.password.reset_request', action_data=action_data,
404 'user.password.reset_request', action_data=action_data,
405 user=self._rhodecode_user, commit=True)
405 user=self._rhodecode_user, commit=True)
406 return HTTPFound(self.request.route_path('reset_password'))
406 return HTTPFound(self.request.route_path('reset_password'))
407
407
408 except formencode.Invalid as errors:
408 except formencode.Invalid as errors:
409 template_context.update({
409 template_context.update({
410 'defaults': errors.value,
410 'defaults': errors.value,
411 'errors': errors.error_dict,
411 'errors': errors.error_dict,
412 })
412 })
413 if not self.request.POST.get('email'):
413 if not self.request.POST.get('email'):
414 # case of empty email, we want to report that
414 # case of empty email, we want to report that
415 return self._get_template_context(c, **template_context)
415 return self._get_template_context(c, **template_context)
416
416
417 if 'recaptcha_field' in errors.error_dict:
417 if 'recaptcha_field' in errors.error_dict:
418 # case of failed captcha
418 # case of failed captcha
419 return self._get_template_context(c, **template_context)
419 return self._get_template_context(c, **template_context)
420
420
421 log.debug('faking response on invalid password reset')
421 log.debug('faking response on invalid password reset')
422 # make this take 2s, to prevent brute forcing.
422 # make this take 2s, to prevent brute forcing.
423 time.sleep(2)
423 time.sleep(2)
424 h.flash(msg, category='success')
424 h.flash(msg, category='success')
425 return HTTPFound(self.request.route_path('reset_password'))
425 return HTTPFound(self.request.route_path('reset_password'))
426
426
427 return self._get_template_context(c, **template_context)
427 return self._get_template_context(c, **template_context)
428
428
429 @view_config(route_name='reset_password_confirmation',
429 @view_config(route_name='reset_password_confirmation',
430 request_method='GET')
430 request_method='GET')
431 def password_reset_confirmation(self):
431 def password_reset_confirmation(self):
432 self.load_default_context()
432 self.load_default_context()
433 if self.request.GET and self.request.GET.get('key'):
433 if self.request.GET and self.request.GET.get('key'):
434 # make this take 2s, to prevent brute forcing.
434 # make this take 2s, to prevent brute forcing.
435 time.sleep(2)
435 time.sleep(2)
436
436
437 token = AuthTokenModel().get_auth_token(
437 token = AuthTokenModel().get_auth_token(
438 self.request.GET.get('key'))
438 self.request.GET.get('key'))
439
439
440 # verify token is the correct role
440 # verify token is the correct role
441 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
441 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
442 log.debug('Got token with role:%s expected is %s',
442 log.debug('Got token with role:%s expected is %s',
443 getattr(token, 'role', 'EMPTY_TOKEN'),
443 getattr(token, 'role', 'EMPTY_TOKEN'),
444 UserApiKeys.ROLE_PASSWORD_RESET)
444 UserApiKeys.ROLE_PASSWORD_RESET)
445 h.flash(
445 h.flash(
446 _('Given reset token is invalid'), category='error')
446 _('Given reset token is invalid'), category='error')
447 return HTTPFound(self.request.route_path('reset_password'))
447 return HTTPFound(self.request.route_path('reset_password'))
448
448
449 try:
449 try:
450 owner = token.user
450 owner = token.user
451 data = {'email': owner.email, 'token': token.api_key}
451 data = {'email': owner.email, 'token': token.api_key}
452 UserModel().reset_password(data)
452 UserModel().reset_password(data)
453 h.flash(
453 h.flash(
454 _('Your password reset was successful, '
454 _('Your password reset was successful, '
455 'a new password has been sent to your email'),
455 'a new password has been sent to your email'),
456 category='success')
456 category='success')
457 except Exception as e:
457 except Exception as e:
458 log.error(e)
458 log.error(e)
459 return HTTPFound(self.request.route_path('reset_password'))
459 return HTTPFound(self.request.route_path('reset_password'))
460
460
461 return HTTPFound(self.request.route_path('login'))
461 return HTTPFound(self.request.route_path('login'))
@@ -1,313 +1,313 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
34 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 from rhodecode.lib.vcs.exceptions import (
36 from rhodecode.lib.vcs.exceptions import (
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 NodeDoesNotExistError)
38 NodeDoesNotExistError)
39 from rhodecode.model.db import Repository, ChangesetStatus
39 from rhodecode.model.db import Repository, ChangesetStatus
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class RepoCompareView(RepoAppView):
44 class RepoCompareView(RepoAppView):
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context(include_app_defaults=True)
46 c = self._get_local_tmpl_context(include_app_defaults=True)
47
47
48 c.rhodecode_repo = self.rhodecode_vcs_repo
48 c.rhodecode_repo = self.rhodecode_vcs_repo
49
49
50
50
51 return c
51 return c
52
52
53 def _get_commit_or_redirect(
53 def _get_commit_or_redirect(
54 self, ref, ref_type, repo, redirect_after=True, partial=False):
54 self, ref, ref_type, repo, redirect_after=True, partial=False):
55 """
55 """
56 This is a safe way to get a commit. If an error occurs it
56 This is a safe way to get a commit. If an error occurs it
57 redirects to a commit with a proper message. If partial is set
57 redirects to a commit with a proper message. If partial is set
58 then it does not do redirect raise and throws an exception instead.
58 then it does not do redirect raise and throws an exception instead.
59 """
59 """
60 _ = self.request.translate
60 _ = self.request.translate
61 try:
61 try:
62 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
62 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
63 except EmptyRepositoryError:
63 except EmptyRepositoryError:
64 if not redirect_after:
64 if not redirect_after:
65 return repo.scm_instance().EMPTY_COMMIT
65 return repo.scm_instance().EMPTY_COMMIT
66 h.flash(h.literal(_('There are no commits yet')),
66 h.flash(h.literal(_('There are no commits yet')),
67 category='warning')
67 category='warning')
68 if not partial:
68 if not partial:
69 raise HTTPFound(
69 raise HTTPFound(
70 h.route_path('repo_summary', repo_name=repo.repo_name))
70 h.route_path('repo_summary', repo_name=repo.repo_name))
71 raise HTTPBadRequest()
71 raise HTTPBadRequest()
72
72
73 except RepositoryError as e:
73 except RepositoryError as e:
74 log.exception(safe_str(e))
74 log.exception(safe_str(e))
75 h.flash(safe_str(h.escape(e)), category='warning')
75 h.flash(safe_str(h.escape(e)), category='warning')
76 if not partial:
76 if not partial:
77 raise HTTPFound(
77 raise HTTPFound(
78 h.route_path('repo_summary', repo_name=repo.repo_name))
78 h.route_path('repo_summary', repo_name=repo.repo_name))
79 raise HTTPBadRequest()
79 raise HTTPBadRequest()
80
80
81 @LoginRequired()
81 @LoginRequired()
82 @HasRepoPermissionAnyDecorator(
82 @HasRepoPermissionAnyDecorator(
83 'repository.read', 'repository.write', 'repository.admin')
83 'repository.read', 'repository.write', 'repository.admin')
84 @view_config(
84 @view_config(
85 route_name='repo_compare_select', request_method='GET',
85 route_name='repo_compare_select', request_method='GET',
86 renderer='rhodecode:templates/compare/compare_diff.mako')
86 renderer='rhodecode:templates/compare/compare_diff.mako')
87 def compare_select(self):
87 def compare_select(self):
88 _ = self.request.translate
88 _ = self.request.translate
89 c = self.load_default_context()
89 c = self.load_default_context()
90
90
91 source_repo = self.db_repo_name
91 source_repo = self.db_repo_name
92 target_repo = self.request.GET.get('target_repo', source_repo)
92 target_repo = self.request.GET.get('target_repo', source_repo)
93 c.source_repo = Repository.get_by_repo_name(source_repo)
93 c.source_repo = Repository.get_by_repo_name(source_repo)
94 c.target_repo = Repository.get_by_repo_name(target_repo)
94 c.target_repo = Repository.get_by_repo_name(target_repo)
95
95
96 if c.source_repo is None or c.target_repo is None:
96 if c.source_repo is None or c.target_repo is None:
97 raise HTTPNotFound()
97 raise HTTPNotFound()
98
98
99 c.compare_home = True
99 c.compare_home = True
100 c.commit_ranges = []
100 c.commit_ranges = []
101 c.collapse_all_commits = False
101 c.collapse_all_commits = False
102 c.diffset = None
102 c.diffset = None
103 c.limited_diff = False
103 c.limited_diff = False
104 c.source_ref = c.target_ref = _('Select commit')
104 c.source_ref = c.target_ref = _('Select commit')
105 c.source_ref_type = ""
105 c.source_ref_type = ""
106 c.target_ref_type = ""
106 c.target_ref_type = ""
107 c.commit_statuses = ChangesetStatus.STATUSES
107 c.commit_statuses = ChangesetStatus.STATUSES
108 c.preview_mode = False
108 c.preview_mode = False
109 c.file_path = None
109 c.file_path = None
110
110
111 return self._get_template_context(c)
111 return self._get_template_context(c)
112
112
113 @LoginRequired()
113 @LoginRequired()
114 @HasRepoPermissionAnyDecorator(
114 @HasRepoPermissionAnyDecorator(
115 'repository.read', 'repository.write', 'repository.admin')
115 'repository.read', 'repository.write', 'repository.admin')
116 @view_config(
116 @view_config(
117 route_name='repo_compare', request_method='GET',
117 route_name='repo_compare', request_method='GET',
118 renderer=None)
118 renderer=None)
119 def compare(self):
119 def compare(self):
120 _ = self.request.translate
120 _ = self.request.translate
121 c = self.load_default_context()
121 c = self.load_default_context()
122
122
123 source_ref_type = self.request.matchdict['source_ref_type']
123 source_ref_type = self.request.matchdict['source_ref_type']
124 source_ref = self.request.matchdict['source_ref']
124 source_ref = self.request.matchdict['source_ref']
125 target_ref_type = self.request.matchdict['target_ref_type']
125 target_ref_type = self.request.matchdict['target_ref_type']
126 target_ref = self.request.matchdict['target_ref']
126 target_ref = self.request.matchdict['target_ref']
127
127
128 # source_ref will be evaluated in source_repo
128 # source_ref will be evaluated in source_repo
129 source_repo_name = self.db_repo_name
129 source_repo_name = self.db_repo_name
130 source_path, source_id = parse_path_ref(source_ref)
130 source_path, source_id = parse_path_ref(source_ref)
131
131
132 # target_ref will be evaluated in target_repo
132 # target_ref will be evaluated in target_repo
133 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
133 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
134 target_path, target_id = parse_path_ref(
134 target_path, target_id = parse_path_ref(
135 target_ref, default_path=self.request.GET.get('f_path', ''))
135 target_ref, default_path=self.request.GET.get('f_path', ''))
136
136
137 # if merge is True
137 # if merge is True
138 # Show what changes since the shared ancestor commit of target/source
138 # Show what changes since the shared ancestor commit of target/source
139 # the source would get if it was merged with target. Only commits
139 # the source would get if it was merged with target. Only commits
140 # which are in target but not in source will be shown.
140 # which are in target but not in source will be shown.
141 merge = str2bool(self.request.GET.get('merge'))
141 merge = str2bool(self.request.GET.get('merge'))
142 # if merge is False
142 # if merge is False
143 # Show a raw diff of source/target refs even if no ancestor exists
143 # Show a raw diff of source/target refs even if no ancestor exists
144
144
145 # c.fulldiff disables cut_off_limit
145 # c.fulldiff disables cut_off_limit
146 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
146 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
147
147
148 c.file_path = target_path
148 c.file_path = target_path
149 c.commit_statuses = ChangesetStatus.STATUSES
149 c.commit_statuses = ChangesetStatus.STATUSES
150
150
151 # if partial, returns just compare_commits.html (commits log)
151 # if partial, returns just compare_commits.html (commits log)
152 partial = self.request.is_xhr
152 partial = self.request.is_xhr
153
153
154 # swap url for compare_diff page
154 # swap url for compare_diff page
155 c.swap_url = h.route_path(
155 c.swap_url = h.route_path(
156 'repo_compare',
156 'repo_compare',
157 repo_name=target_repo_name,
157 repo_name=target_repo_name,
158 source_ref_type=target_ref_type,
158 source_ref_type=target_ref_type,
159 source_ref=target_ref,
159 source_ref=target_ref,
160 target_repo=source_repo_name,
160 target_repo=source_repo_name,
161 target_ref_type=source_ref_type,
161 target_ref_type=source_ref_type,
162 target_ref=source_ref,
162 target_ref=source_ref,
163 _query=dict(merge=merge and '1' or '', f_path=target_path))
163 _query=dict(merge=merge and '1' or '', f_path=target_path))
164
164
165 source_repo = Repository.get_by_repo_name(source_repo_name)
165 source_repo = Repository.get_by_repo_name(source_repo_name)
166 target_repo = Repository.get_by_repo_name(target_repo_name)
166 target_repo = Repository.get_by_repo_name(target_repo_name)
167
167
168 if source_repo is None:
168 if source_repo is None:
169 log.error('Could not find the source repo: {}'
169 log.error('Could not find the source repo: {}'
170 .format(source_repo_name))
170 .format(source_repo_name))
171 h.flash(_('Could not find the source repo: `{}`')
171 h.flash(_('Could not find the source repo: `{}`')
172 .format(h.escape(source_repo_name)), category='error')
172 .format(h.escape(source_repo_name)), category='error')
173 raise HTTPFound(
173 raise HTTPFound(
174 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
174 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
175
175
176 if target_repo is None:
176 if target_repo is None:
177 log.error('Could not find the target repo: {}'
177 log.error('Could not find the target repo: {}'
178 .format(source_repo_name))
178 .format(source_repo_name))
179 h.flash(_('Could not find the target repo: `{}`')
179 h.flash(_('Could not find the target repo: `{}`')
180 .format(h.escape(target_repo_name)), category='error')
180 .format(h.escape(target_repo_name)), category='error')
181 raise HTTPFound(
181 raise HTTPFound(
182 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
182 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
183
183
184 source_scm = source_repo.scm_instance()
184 source_scm = source_repo.scm_instance()
185 target_scm = target_repo.scm_instance()
185 target_scm = target_repo.scm_instance()
186
186
187 source_alias = source_scm.alias
187 source_alias = source_scm.alias
188 target_alias = target_scm.alias
188 target_alias = target_scm.alias
189 if source_alias != target_alias:
189 if source_alias != target_alias:
190 msg = _('The comparison of two different kinds of remote repos '
190 msg = _('The comparison of two different kinds of remote repos '
191 'is not available')
191 'is not available')
192 log.error(msg)
192 log.error(msg)
193 h.flash(msg, category='error')
193 h.flash(msg, category='error')
194 raise HTTPFound(
194 raise HTTPFound(
195 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
195 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
196
196
197 source_commit = self._get_commit_or_redirect(
197 source_commit = self._get_commit_or_redirect(
198 ref=source_id, ref_type=source_ref_type, repo=source_repo,
198 ref=source_id, ref_type=source_ref_type, repo=source_repo,
199 partial=partial)
199 partial=partial)
200 target_commit = self._get_commit_or_redirect(
200 target_commit = self._get_commit_or_redirect(
201 ref=target_id, ref_type=target_ref_type, repo=target_repo,
201 ref=target_id, ref_type=target_ref_type, repo=target_repo,
202 partial=partial)
202 partial=partial)
203
203
204 c.compare_home = False
204 c.compare_home = False
205 c.source_repo = source_repo
205 c.source_repo = source_repo
206 c.target_repo = target_repo
206 c.target_repo = target_repo
207 c.source_ref = source_ref
207 c.source_ref = source_ref
208 c.target_ref = target_ref
208 c.target_ref = target_ref
209 c.source_ref_type = source_ref_type
209 c.source_ref_type = source_ref_type
210 c.target_ref_type = target_ref_type
210 c.target_ref_type = target_ref_type
211
211
212 pre_load = ["author", "branch", "date", "message"]
212 pre_load = ["author", "branch", "date", "message"]
213 c.ancestor = None
213 c.ancestor = None
214
214
215 if c.file_path:
215 if c.file_path:
216 if source_commit == target_commit:
216 if source_commit == target_commit:
217 c.commit_ranges = []
217 c.commit_ranges = []
218 else:
218 else:
219 c.commit_ranges = [target_commit]
219 c.commit_ranges = [target_commit]
220 else:
220 else:
221 try:
221 try:
222 c.commit_ranges = source_scm.compare(
222 c.commit_ranges = source_scm.compare(
223 source_commit.raw_id, target_commit.raw_id,
223 source_commit.raw_id, target_commit.raw_id,
224 target_scm, merge, pre_load=pre_load)
224 target_scm, merge, pre_load=pre_load)
225 if merge:
225 if merge:
226 c.ancestor = source_scm.get_common_ancestor(
226 c.ancestor = source_scm.get_common_ancestor(
227 source_commit.raw_id, target_commit.raw_id, target_scm)
227 source_commit.raw_id, target_commit.raw_id, target_scm)
228 except RepositoryRequirementError:
228 except RepositoryRequirementError:
229 msg = _('Could not compare repos with different '
229 msg = _('Could not compare repos with different '
230 'large file settings')
230 'large file settings')
231 log.error(msg)
231 log.error(msg)
232 if partial:
232 if partial:
233 return Response(msg)
233 return Response(msg)
234 h.flash(msg, category='error')
234 h.flash(msg, category='error')
235 raise HTTPFound(
235 raise HTTPFound(
236 h.route_path('repo_compare_select',
236 h.route_path('repo_compare_select',
237 repo_name=self.db_repo_name))
237 repo_name=self.db_repo_name))
238
238
239 c.statuses = self.db_repo.statuses(
239 c.statuses = self.db_repo.statuses(
240 [x.raw_id for x in c.commit_ranges])
240 [x.raw_id for x in c.commit_ranges])
241
241
242 # auto collapse if we have more than limit
242 # auto collapse if we have more than limit
243 collapse_limit = diffs.DiffProcessor._collapse_commits_over
243 collapse_limit = diffs.DiffProcessor._collapse_commits_over
244 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
244 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
245
245
246 if partial: # for PR ajax commits loader
246 if partial: # for PR ajax commits loader
247 if not c.ancestor:
247 if not c.ancestor:
248 return Response('') # cannot merge if there is no ancestor
248 return Response('') # cannot merge if there is no ancestor
249
249
250 html = render(
250 html = render(
251 'rhodecode:templates/compare/compare_commits.mako',
251 'rhodecode:templates/compare/compare_commits.mako',
252 self._get_template_context(c), self.request)
252 self._get_template_context(c), self.request)
253 return Response(html)
253 return Response(html)
254
254
255 if c.ancestor:
255 if c.ancestor:
256 # case we want a simple diff without incoming commits,
256 # case we want a simple diff without incoming commits,
257 # previewing what will be merged.
257 # previewing what will be merged.
258 # Make the diff on target repo (which is known to have target_ref)
258 # Make the diff on target repo (which is known to have target_ref)
259 log.debug('Using ancestor %s as source_ref instead of %s'
259 log.debug('Using ancestor %s as source_ref instead of %s',
260 % (c.ancestor, source_ref))
260 c.ancestor, source_ref)
261 source_repo = target_repo
261 source_repo = target_repo
262 source_commit = target_repo.get_commit(commit_id=c.ancestor)
262 source_commit = target_repo.get_commit(commit_id=c.ancestor)
263
263
264 # diff_limit will cut off the whole diff if the limit is applied
264 # diff_limit will cut off the whole diff if the limit is applied
265 # otherwise it will just hide the big files from the front-end
265 # otherwise it will just hide the big files from the front-end
266 diff_limit = c.visual.cut_off_limit_diff
266 diff_limit = c.visual.cut_off_limit_diff
267 file_limit = c.visual.cut_off_limit_file
267 file_limit = c.visual.cut_off_limit_file
268
268
269 log.debug('calculating diff between '
269 log.debug('calculating diff between '
270 'source_ref:%s and target_ref:%s for repo `%s`',
270 'source_ref:%s and target_ref:%s for repo `%s`',
271 source_commit, target_commit,
271 source_commit, target_commit,
272 safe_unicode(source_repo.scm_instance().path))
272 safe_unicode(source_repo.scm_instance().path))
273
273
274 if source_commit.repository != target_commit.repository:
274 if source_commit.repository != target_commit.repository:
275 msg = _(
275 msg = _(
276 "Repositories unrelated. "
276 "Repositories unrelated. "
277 "Cannot compare commit %(commit1)s from repository %(repo1)s "
277 "Cannot compare commit %(commit1)s from repository %(repo1)s "
278 "with commit %(commit2)s from repository %(repo2)s.") % {
278 "with commit %(commit2)s from repository %(repo2)s.") % {
279 'commit1': h.show_id(source_commit),
279 'commit1': h.show_id(source_commit),
280 'repo1': source_repo.repo_name,
280 'repo1': source_repo.repo_name,
281 'commit2': h.show_id(target_commit),
281 'commit2': h.show_id(target_commit),
282 'repo2': target_repo.repo_name,
282 'repo2': target_repo.repo_name,
283 }
283 }
284 h.flash(msg, category='error')
284 h.flash(msg, category='error')
285 raise HTTPFound(
285 raise HTTPFound(
286 h.route_path('repo_compare_select',
286 h.route_path('repo_compare_select',
287 repo_name=self.db_repo_name))
287 repo_name=self.db_repo_name))
288
288
289 txt_diff = source_repo.scm_instance().get_diff(
289 txt_diff = source_repo.scm_instance().get_diff(
290 commit1=source_commit, commit2=target_commit,
290 commit1=source_commit, commit2=target_commit,
291 path=target_path, path1=source_path)
291 path=target_path, path1=source_path)
292
292
293 diff_processor = diffs.DiffProcessor(
293 diff_processor = diffs.DiffProcessor(
294 txt_diff, format='newdiff', diff_limit=diff_limit,
294 txt_diff, format='newdiff', diff_limit=diff_limit,
295 file_limit=file_limit, show_full_diff=c.fulldiff)
295 file_limit=file_limit, show_full_diff=c.fulldiff)
296 _parsed = diff_processor.prepare()
296 _parsed = diff_processor.prepare()
297
297
298 diffset = codeblocks.DiffSet(
298 diffset = codeblocks.DiffSet(
299 repo_name=source_repo.repo_name,
299 repo_name=source_repo.repo_name,
300 source_node_getter=codeblocks.diffset_node_getter(source_commit),
300 source_node_getter=codeblocks.diffset_node_getter(source_commit),
301 target_node_getter=codeblocks.diffset_node_getter(target_commit),
301 target_node_getter=codeblocks.diffset_node_getter(target_commit),
302 )
302 )
303 c.diffset = self.path_filter.render_patchset_filtered(
303 c.diffset = self.path_filter.render_patchset_filtered(
304 diffset, _parsed, source_ref, target_ref)
304 diffset, _parsed, source_ref, target_ref)
305
305
306 c.preview_mode = merge
306 c.preview_mode = merge
307 c.source_commit = source_commit
307 c.source_commit = source_commit
308 c.target_commit = target_commit
308 c.target_commit = target_commit
309
309
310 html = render(
310 html = render(
311 'rhodecode:templates/compare/compare_diff.mako',
311 'rhodecode:templates/compare/compare_diff.mako',
312 self._get_template_context(c), self.request)
312 self._get_template_context(c), self.request)
313 return Response(html) No newline at end of file
313 return Response(html)
@@ -1,113 +1,113 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 from pyramid.view import view_config
22 from pyramid.view import view_config
23
23
24 from rhodecode.apps._base import RepoAppView
24 from rhodecode.apps._base import RepoAppView
25 from rhodecode.lib import audit_logger
25 from rhodecode.lib import audit_logger
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
28 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
28 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class StripView(RepoAppView):
34 class StripView(RepoAppView):
35 def load_default_context(self):
35 def load_default_context(self):
36 c = self._get_local_tmpl_context()
36 c = self._get_local_tmpl_context()
37
37
38
38
39 return c
39 return c
40
40
41 @LoginRequired()
41 @LoginRequired()
42 @HasRepoPermissionAnyDecorator('repository.admin')
42 @HasRepoPermissionAnyDecorator('repository.admin')
43 @view_config(
43 @view_config(
44 route_name='edit_repo_strip', request_method='GET',
44 route_name='edit_repo_strip', request_method='GET',
45 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
45 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
46 def strip(self):
46 def strip(self):
47 c = self.load_default_context()
47 c = self.load_default_context()
48 c.active = 'strip'
48 c.active = 'strip'
49 c.strip_limit = 10
49 c.strip_limit = 10
50
50
51 return self._get_template_context(c)
51 return self._get_template_context(c)
52
52
53 @LoginRequired()
53 @LoginRequired()
54 @HasRepoPermissionAnyDecorator('repository.admin')
54 @HasRepoPermissionAnyDecorator('repository.admin')
55 @CSRFRequired()
55 @CSRFRequired()
56 @view_config(
56 @view_config(
57 route_name='strip_check', request_method='POST',
57 route_name='strip_check', request_method='POST',
58 renderer='json', xhr=True)
58 renderer='json', xhr=True)
59 def strip_check(self):
59 def strip_check(self):
60 from rhodecode.lib.vcs.backends.base import EmptyCommit
60 from rhodecode.lib.vcs.backends.base import EmptyCommit
61 data = {}
61 data = {}
62 rp = self.request.POST
62 rp = self.request.POST
63 for i in range(1, 11):
63 for i in range(1, 11):
64 chset = 'changeset_id-%d' % (i,)
64 chset = 'changeset_id-%d' % (i,)
65 check = rp.get(chset)
65 check = rp.get(chset)
66
66
67 if check:
67 if check:
68 data[i] = self.db_repo.get_changeset(rp[chset])
68 data[i] = self.db_repo.get_changeset(rp[chset])
69 if isinstance(data[i], EmptyCommit):
69 if isinstance(data[i], EmptyCommit):
70 data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
70 data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
71 else:
71 else:
72 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
72 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
73 'author': h.escape(data[i].author),
73 'author': h.escape(data[i].author),
74 'comment': h.escape(data[i].message)}
74 'comment': h.escape(data[i].message)}
75 else:
75 else:
76 break
76 break
77 return data
77 return data
78
78
79 @LoginRequired()
79 @LoginRequired()
80 @HasRepoPermissionAnyDecorator('repository.admin')
80 @HasRepoPermissionAnyDecorator('repository.admin')
81 @CSRFRequired()
81 @CSRFRequired()
82 @view_config(
82 @view_config(
83 route_name='strip_execute', request_method='POST',
83 route_name='strip_execute', request_method='POST',
84 renderer='json', xhr=True)
84 renderer='json', xhr=True)
85 def strip_execute(self):
85 def strip_execute(self):
86 from rhodecode.model.scm import ScmModel
86 from rhodecode.model.scm import ScmModel
87
87
88 c = self.load_default_context()
88 c = self.load_default_context()
89 user = self._rhodecode_user
89 user = self._rhodecode_user
90 rp = self.request.POST
90 rp = self.request.POST
91 data = {}
91 data = {}
92 for idx in rp:
92 for idx in rp:
93 commit = json.loads(rp[idx])
93 commit = json.loads(rp[idx])
94 # If someone put two times the same branch
94 # If someone put two times the same branch
95 if commit['branch'] in data.keys():
95 if commit['branch'] in data.keys():
96 continue
96 continue
97 try:
97 try:
98 ScmModel().strip(
98 ScmModel().strip(
99 repo=self.db_repo,
99 repo=self.db_repo,
100 commit_id=commit['rev'], branch=commit['branch'])
100 commit_id=commit['rev'], branch=commit['branch'])
101 log.info('Stripped commit %s from repo `%s` by %s' % (
101 log.info('Stripped commit %s from repo `%s` by %s',
102 commit['rev'], self.db_repo_name, user))
102 commit['rev'], self.db_repo_name, user)
103 data[commit['rev']] = True
103 data[commit['rev']] = True
104
104
105 audit_logger.store_web(
105 audit_logger.store_web(
106 'repo.commit.strip', action_data={'commit_id': commit['rev']},
106 'repo.commit.strip', action_data={'commit_id': commit['rev']},
107 repo=self.db_repo, user=self._rhodecode_user, commit=True)
107 repo=self.db_repo, user=self._rhodecode_user, commit=True)
108
108
109 except Exception as e:
109 except Exception as e:
110 data[commit['rev']] = False
110 data[commit['rev']] = False
111 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (
111 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s',
112 commit['rev'], self.db_repo_name, user, e.message))
112 commit['rev'], self.db_repo_name, user, e.message)
113 return data
113 return data
@@ -1,285 +1,285 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 RhodeCode authentication plugin for Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib2
29 import urllib2
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if not "port" in kwargs:
113 if not "port" in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib2.build_opener(handler)
134 self.opener = urllib2.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib2.Request(url, body, _headers)
149 request = urllib2.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 rdoc = self.opener.open(request)
156 rdoc = self.opener.open(request)
157 msg = "".join(rdoc.readlines())
157 msg = "".join(rdoc.readlines())
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 rval = {}
159 rval = {}
160 rval["status"] = True
160 rval["status"] = True
161 rval["error"] = "Response body was empty"
161 rval["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 rval = json.loads(msg)
163 rval = json.loads(msg)
164 rval["status"] = True
164 rval["status"] = True
165 else:
165 else:
166 rval = "".join(rdoc.readlines())
166 rval = "".join(rdoc.readlines())
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 rval = {"status": False,
169 rval = {"status": False,
170 "body": body,
170 "body": body,
171 "error": str(e) + "\n" + msg}
171 "error": str(e) + "\n" + msg}
172 else:
172 else:
173 rval = None
173 rval = None
174 return rval
174 return rval
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 _settings_unsafe_keys = ['app_password']
192 _settings_unsafe_keys = ['app_password']
193
193
194 def includeme(self, config):
194 def includeme(self, config):
195 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
197 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
201 request_method='GET',
202 route_name='auth_home',
202 route_name='auth_home',
203 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
204 config.add_view(
204 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
208 request_method='POST',
209 route_name='auth_home',
209 route_name='auth_home',
210 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
211
211
212 def get_settings_schema(self):
212 def get_settings_schema(self):
213 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
214
214
215 def get_display_name(self):
215 def get_display_name(self):
216 return _('CROWD')
216 return _('CROWD')
217
217
218 @hybrid_property
218 @hybrid_property
219 def name(self):
219 def name(self):
220 return "crowd"
220 return "crowd"
221
221
222 def use_fake_password(self):
222 def use_fake_password(self):
223 return True
223 return True
224
224
225 def user_activation_state(self):
225 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
227 return 'hg.extern_activate.auto' in def_user_perms
227 return 'hg.extern_activate.auto' in def_user_perms
228
228
229 def auth(self, userobj, username, password, settings, **kwargs):
229 def auth(self, userobj, username, password, settings, **kwargs):
230 """
230 """
231 Given a user object (which may be null), username, a plaintext password,
231 Given a user object (which may be null), username, a plaintext password,
232 and a settings object (containing all the keys needed as listed in settings()),
232 and a settings object (containing all the keys needed as listed in settings()),
233 authenticate this user's login attempt.
233 authenticate this user's login attempt.
234
234
235 Return None on failure. On success, return a dictionary of the form:
235 Return None on failure. On success, return a dictionary of the form:
236
236
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
238 This is later validated for correctness
239 """
239 """
240 if not username or not password:
240 if not username or not password:
241 log.debug('Empty username or password skipping...')
241 log.debug('Empty username or password skipping...')
242 return None
242 return None
243
243
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 log.debug("Crowd settings: \n%s", formatted_json(settings))
245 server = CrowdServer(**settings)
245 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
246 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
247 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
249 if not crowd_user["status"]:
249 if not crowd_user["status"]:
250 return None
250 return None
251
251
252 res = server.user_groups(crowd_user["name"])
252 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 log.debug("Crowd groups: \n%s", formatted_json(res))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
255
256 # old attrs fetched from RhodeCode database
256 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
257 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
258 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
259 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
260 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
261 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
262 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
263 extern_type = getattr(userobj, 'extern_type', '')
264
264
265 user_attrs = {
265 user_attrs = {
266 'username': username,
266 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
267 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
268 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
269 'groups': crowd_user["groups"],
270 'user_group_sync': True,
270 'user_group_sync': True,
271 'email': crowd_user["email"] or email,
271 'email': crowd_user["email"] or email,
272 'admin': admin,
272 'admin': admin,
273 'active': active,
273 'active': active,
274 'active_from_extern': crowd_user.get('active'),
274 'active_from_extern': crowd_user.get('active'),
275 'extern_name': crowd_user["name"],
275 'extern_name': crowd_user["name"],
276 'extern_type': extern_type,
276 'extern_type': extern_type,
277 }
277 }
278
278
279 # set an admin if we're in admin_groups of crowd
279 # set an admin if we're in admin_groups of crowd
280 for group in settings["admin_groups"]:
280 for group in settings["admin_groups"]:
281 if group in user_attrs["groups"]:
281 if group in user_attrs["groups"]:
282 user_attrs["admin"] = True
282 user_attrs["admin"] = True
283 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
284 log.info('user `%s` authenticated correctly' % user_attrs['username'])
284 log.info('user `%s` authenticated correctly', user_attrs['username'])
285 return user_attrs
285 return user_attrs
@@ -1,225 +1,225 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 colander
21 import colander
22 import logging
22 import logging
23
23
24 from rhodecode.translation import _
24 from rhodecode.translation import _
25 from rhodecode.authentication.base import (
25 from rhodecode.authentication.base import (
26 RhodeCodeExternalAuthPlugin, hybrid_property)
26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 from rhodecode.lib.colander_utils import strip_whitespace
29 from rhodecode.lib.colander_utils import strip_whitespace
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 """
38 """
39 Factory function that is called during plugin discovery.
39 Factory function that is called during plugin discovery.
40 It returns the plugin instance.
40 It returns the plugin instance.
41 """
41 """
42 plugin = RhodeCodeAuthPlugin(plugin_id)
42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 return plugin
43 return plugin
44
44
45
45
46 class HeadersAuthnResource(AuthnPluginResourceBase):
46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 pass
47 pass
48
48
49
49
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 header = colander.SchemaNode(
51 header = colander.SchemaNode(
52 colander.String(),
52 colander.String(),
53 default='REMOTE_USER',
53 default='REMOTE_USER',
54 description=_('Header to extract the user from'),
54 description=_('Header to extract the user from'),
55 preparer=strip_whitespace,
55 preparer=strip_whitespace,
56 title=_('Header'),
56 title=_('Header'),
57 widget='string')
57 widget='string')
58 fallback_header = colander.SchemaNode(
58 fallback_header = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 default='HTTP_X_FORWARDED_USER',
60 default='HTTP_X_FORWARDED_USER',
61 description=_('Header to extract the user from when main one fails'),
61 description=_('Header to extract the user from when main one fails'),
62 preparer=strip_whitespace,
62 preparer=strip_whitespace,
63 title=_('Fallback header'),
63 title=_('Fallback header'),
64 widget='string')
64 widget='string')
65 clean_username = colander.SchemaNode(
65 clean_username = colander.SchemaNode(
66 colander.Boolean(),
66 colander.Boolean(),
67 default=True,
67 default=True,
68 description=_('Perform cleaning of user, if passed user has @ in '
68 description=_('Perform cleaning of user, if passed user has @ in '
69 'username then first part before @ is taken. '
69 'username then first part before @ is taken. '
70 'If there\'s \\ in the username only the part after '
70 'If there\'s \\ in the username only the part after '
71 ' \\ is taken'),
71 ' \\ is taken'),
72 missing=False,
72 missing=False,
73 title=_('Clean username'),
73 title=_('Clean username'),
74 widget='bool')
74 widget='bool')
75
75
76
76
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=HeadersAuthnResource)
88 context=HeadersAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=HeadersAuthnResource)
95 context=HeadersAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('Headers')
98 return _('Headers')
99
99
100 def get_settings_schema(self):
100 def get_settings_schema(self):
101 return HeadersSettingsSchema()
101 return HeadersSettingsSchema()
102
102
103 @hybrid_property
103 @hybrid_property
104 def name(self):
104 def name(self):
105 return 'headers'
105 return 'headers'
106
106
107 @property
107 @property
108 def is_headers_auth(self):
108 def is_headers_auth(self):
109 return True
109 return True
110
110
111 def use_fake_password(self):
111 def use_fake_password(self):
112 return True
112 return True
113
113
114 def user_activation_state(self):
114 def user_activation_state(self):
115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 return 'hg.extern_activate.auto' in def_user_perms
116 return 'hg.extern_activate.auto' in def_user_perms
117
117
118 def _clean_username(self, username):
118 def _clean_username(self, username):
119 # Removing realm and domain from username
119 # Removing realm and domain from username
120 username = username.split('@')[0]
120 username = username.split('@')[0]
121 username = username.rsplit('\\')[-1]
121 username = username.rsplit('\\')[-1]
122 return username
122 return username
123
123
124 def _get_username(self, environ, settings):
124 def _get_username(self, environ, settings):
125 username = None
125 username = None
126 environ = environ or {}
126 environ = environ or {}
127 if not environ:
127 if not environ:
128 log.debug('got empty environ: %s' % environ)
128 log.debug('got empty environ: %s', environ)
129
129
130 settings = settings or {}
130 settings = settings or {}
131 if settings.get('header'):
131 if settings.get('header'):
132 header = settings.get('header')
132 header = settings.get('header')
133 username = environ.get(header)
133 username = environ.get(header)
134 log.debug('extracted %s:%s' % (header, username))
134 log.debug('extracted %s:%s', header, username)
135
135
136 # fallback mode
136 # fallback mode
137 if not username and settings.get('fallback_header'):
137 if not username and settings.get('fallback_header'):
138 header = settings.get('fallback_header')
138 header = settings.get('fallback_header')
139 username = environ.get(header)
139 username = environ.get(header)
140 log.debug('extracted %s:%s' % (header, username))
140 log.debug('extracted %s:%s', header, username)
141
141
142 if username and str2bool(settings.get('clean_username')):
142 if username and str2bool(settings.get('clean_username')):
143 log.debug('Received username `%s` from headers' % username)
143 log.debug('Received username `%s` from headers', username)
144 username = self._clean_username(username)
144 username = self._clean_username(username)
145 log.debug('New cleanup user is:%s' % username)
145 log.debug('New cleanup user is:%s', username)
146 return username
146 return username
147
147
148 def get_user(self, username=None, **kwargs):
148 def get_user(self, username=None, **kwargs):
149 """
149 """
150 Helper method for user fetching in plugins, by default it's using
150 Helper method for user fetching in plugins, by default it's using
151 simple fetch by username, but this method can be custimized in plugins
151 simple fetch by username, but this method can be custimized in plugins
152 eg. headers auth plugin to fetch user by environ params
152 eg. headers auth plugin to fetch user by environ params
153 :param username: username if given to fetch
153 :param username: username if given to fetch
154 :param kwargs: extra arguments needed for user fetching.
154 :param kwargs: extra arguments needed for user fetching.
155 """
155 """
156 environ = kwargs.get('environ') or {}
156 environ = kwargs.get('environ') or {}
157 settings = kwargs.get('settings') or {}
157 settings = kwargs.get('settings') or {}
158 username = self._get_username(environ, settings)
158 username = self._get_username(environ, settings)
159 # we got the username, so use default method now
159 # we got the username, so use default method now
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
161
161
162 def auth(self, userobj, username, password, settings, **kwargs):
162 def auth(self, userobj, username, password, settings, **kwargs):
163 """
163 """
164 Get's the headers_auth username (or email). It tries to get username
164 Get's the headers_auth username (or email). It tries to get username
165 from REMOTE_USER if this plugin is enabled, if that fails
165 from REMOTE_USER if this plugin is enabled, if that fails
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 is set. clean_username extracts the username from this data if it's
167 is set. clean_username extracts the username from this data if it's
168 having @ in it.
168 having @ in it.
169 Return None on failure. On success, return a dictionary of the form:
169 Return None on failure. On success, return a dictionary of the form:
170
170
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
172
172
173 :param userobj:
173 :param userobj:
174 :param username:
174 :param username:
175 :param password:
175 :param password:
176 :param settings:
176 :param settings:
177 :param kwargs:
177 :param kwargs:
178 """
178 """
179 environ = kwargs.get('environ')
179 environ = kwargs.get('environ')
180 if not environ:
180 if not environ:
181 log.debug('Empty environ data skipping...')
181 log.debug('Empty environ data skipping...')
182 return None
182 return None
183
183
184 if not userobj:
184 if not userobj:
185 userobj = self.get_user('', environ=environ, settings=settings)
185 userobj = self.get_user('', environ=environ, settings=settings)
186
186
187 # we don't care passed username/password for headers auth plugins.
187 # we don't care passed username/password for headers auth plugins.
188 # only way to log in is using environ
188 # only way to log in is using environ
189 username = None
189 username = None
190 if userobj:
190 if userobj:
191 username = getattr(userobj, 'username')
191 username = getattr(userobj, 'username')
192
192
193 if not username:
193 if not username:
194 # we don't have any objects in DB user doesn't exist extract
194 # we don't have any objects in DB user doesn't exist extract
195 # username from environ based on the settings
195 # username from environ based on the settings
196 username = self._get_username(environ, settings)
196 username = self._get_username(environ, settings)
197
197
198 # if cannot fetch username, it's a no-go for this plugin to proceed
198 # if cannot fetch username, it's a no-go for this plugin to proceed
199 if not username:
199 if not username:
200 return None
200 return None
201
201
202 # old attrs fetched from RhodeCode database
202 # old attrs fetched from RhodeCode database
203 admin = getattr(userobj, 'admin', False)
203 admin = getattr(userobj, 'admin', False)
204 active = getattr(userobj, 'active', True)
204 active = getattr(userobj, 'active', True)
205 email = getattr(userobj, 'email', '')
205 email = getattr(userobj, 'email', '')
206 firstname = getattr(userobj, 'firstname', '')
206 firstname = getattr(userobj, 'firstname', '')
207 lastname = getattr(userobj, 'lastname', '')
207 lastname = getattr(userobj, 'lastname', '')
208 extern_type = getattr(userobj, 'extern_type', '')
208 extern_type = getattr(userobj, 'extern_type', '')
209
209
210 user_attrs = {
210 user_attrs = {
211 'username': username,
211 'username': username,
212 'firstname': safe_unicode(firstname or username),
212 'firstname': safe_unicode(firstname or username),
213 'lastname': safe_unicode(lastname or ''),
213 'lastname': safe_unicode(lastname or ''),
214 'groups': [],
214 'groups': [],
215 'user_group_sync': False,
215 'user_group_sync': False,
216 'email': email or '',
216 'email': email or '',
217 'admin': admin or False,
217 'admin': admin or False,
218 'active': active,
218 'active': active,
219 'active_from_extern': True,
219 'active_from_extern': True,
220 'extern_name': username,
220 'extern_name': username,
221 'extern_type': extern_type,
221 'extern_type': extern_type,
222 }
222 }
223
223
224 log.info('user `%s` authenticated correctly' % user_attrs['username'])
224 log.info('user `%s` authenticated correctly', user_attrs['username'])
225 return user_attrs
225 return user_attrs
@@ -1,167 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 RhodeCode authentication plugin for Jasig CAS
22 RhodeCode authentication plugin for Jasig CAS
23 http://www.jasig.org/cas
23 http://www.jasig.org/cas
24 """
24 """
25
25
26
26
27 import colander
27 import colander
28 import logging
28 import logging
29 import rhodecode
29 import rhodecode
30 import urllib
30 import urllib
31 import urllib2
31 import urllib2
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def plugin_factory(plugin_id, *args, **kwds):
45 def plugin_factory(plugin_id, *args, **kwds):
46 """
46 """
47 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
48 It returns the plugin instance.
48 It returns the plugin instance.
49 """
49 """
50 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 return plugin
51 return plugin
52
52
53
53
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('URL'),
64 title=_('URL'),
65 widget='string')
65 widget='string')
66
66
67
67
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69
69
70 def includeme(self, config):
70 def includeme(self, config):
71 config.add_authn_plugin(self)
71 config.add_authn_plugin(self)
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_view(
73 config.add_view(
74 'rhodecode.authentication.views.AuthnPluginViewBase',
74 'rhodecode.authentication.views.AuthnPluginViewBase',
75 attr='settings_get',
75 attr='settings_get',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 request_method='GET',
77 request_method='GET',
78 route_name='auth_home',
78 route_name='auth_home',
79 context=JasigCasAuthnResource)
79 context=JasigCasAuthnResource)
80 config.add_view(
80 config.add_view(
81 'rhodecode.authentication.views.AuthnPluginViewBase',
81 'rhodecode.authentication.views.AuthnPluginViewBase',
82 attr='settings_post',
82 attr='settings_post',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 request_method='POST',
84 request_method='POST',
85 route_name='auth_home',
85 route_name='auth_home',
86 context=JasigCasAuthnResource)
86 context=JasigCasAuthnResource)
87
87
88 def get_settings_schema(self):
88 def get_settings_schema(self):
89 return JasigCasSettingsSchema()
89 return JasigCasSettingsSchema()
90
90
91 def get_display_name(self):
91 def get_display_name(self):
92 return _('Jasig-CAS')
92 return _('Jasig-CAS')
93
93
94 @hybrid_property
94 @hybrid_property
95 def name(self):
95 def name(self):
96 return "jasig-cas"
96 return "jasig-cas"
97
97
98 @property
98 @property
99 def is_headers_auth(self):
99 def is_headers_auth(self):
100 return True
100 return True
101
101
102 def use_fake_password(self):
102 def use_fake_password(self):
103 return True
103 return True
104
104
105 def user_activation_state(self):
105 def user_activation_state(self):
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 return 'hg.extern_activate.auto' in def_user_perms
107 return 'hg.extern_activate.auto' in def_user_perms
108
108
109 def auth(self, userobj, username, password, settings, **kwargs):
109 def auth(self, userobj, username, password, settings, **kwargs):
110 """
110 """
111 Given a user object (which may be null), username, a plaintext password,
111 Given a user object (which may be null), username, a plaintext password,
112 and a settings object (containing all the keys needed as listed in settings()),
112 and a settings object (containing all the keys needed as listed in settings()),
113 authenticate this user's login attempt.
113 authenticate this user's login attempt.
114
114
115 Return None on failure. On success, return a dictionary of the form:
115 Return None on failure. On success, return a dictionary of the form:
116
116
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 This is later validated for correctness
118 This is later validated for correctness
119 """
119 """
120 if not username or not password:
120 if not username or not password:
121 log.debug('Empty username or password skipping...')
121 log.debug('Empty username or password skipping...')
122 return None
122 return None
123
123
124 log.debug("Jasig CAS settings: %s", settings)
124 log.debug("Jasig CAS settings: %s", settings)
125 params = urllib.urlencode({'username': username, 'password': password})
125 params = urllib.urlencode({'username': username, 'password': password})
126 headers = {"Content-type": "application/x-www-form-urlencoded",
126 headers = {"Content-type": "application/x-www-form-urlencoded",
127 "Accept": "text/plain",
127 "Accept": "text/plain",
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 url = settings["service_url"]
129 url = settings["service_url"]
130
130
131 log.debug("Sent Jasig CAS: \n%s",
131 log.debug("Sent Jasig CAS: \n%s",
132 {"url": url, "body": params, "headers": headers})
132 {"url": url, "body": params, "headers": headers})
133 request = urllib2.Request(url, params, headers)
133 request = urllib2.Request(url, params, headers)
134 try:
134 try:
135 response = urllib2.urlopen(request)
135 response = urllib2.urlopen(request)
136 except urllib2.HTTPError as e:
136 except urllib2.HTTPError as e:
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
138 return None
138 return None
139 except urllib2.URLError as e:
139 except urllib2.URLError as e:
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
140 log.debug("URLError when requesting Jasig CAS url: %s ", url)
141 return None
141 return None
142
142
143 # old attrs fetched from RhodeCode database
143 # old attrs fetched from RhodeCode database
144 admin = getattr(userobj, 'admin', False)
144 admin = getattr(userobj, 'admin', False)
145 active = getattr(userobj, 'active', True)
145 active = getattr(userobj, 'active', True)
146 email = getattr(userobj, 'email', '')
146 email = getattr(userobj, 'email', '')
147 username = getattr(userobj, 'username', username)
147 username = getattr(userobj, 'username', username)
148 firstname = getattr(userobj, 'firstname', '')
148 firstname = getattr(userobj, 'firstname', '')
149 lastname = getattr(userobj, 'lastname', '')
149 lastname = getattr(userobj, 'lastname', '')
150 extern_type = getattr(userobj, 'extern_type', '')
150 extern_type = getattr(userobj, 'extern_type', '')
151
151
152 user_attrs = {
152 user_attrs = {
153 'username': username,
153 'username': username,
154 'firstname': safe_unicode(firstname or username),
154 'firstname': safe_unicode(firstname or username),
155 'lastname': safe_unicode(lastname or ''),
155 'lastname': safe_unicode(lastname or ''),
156 'groups': [],
156 'groups': [],
157 'user_group_sync': False,
157 'user_group_sync': False,
158 'email': email or '',
158 'email': email or '',
159 'admin': admin or False,
159 'admin': admin or False,
160 'active': active,
160 'active': active,
161 'active_from_extern': True,
161 'active_from_extern': True,
162 'extern_name': username,
162 'extern_name': username,
163 'extern_type': extern_type,
163 'extern_type': extern_type,
164 }
164 }
165
165
166 log.info('user `%s` authenticated correctly' % user_attrs['username'])
166 log.info('user `%s` authenticated correctly', user_attrs['username'])
167 return user_attrs
167 return user_attrs
@@ -1,161 +1,161 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 RhodeCode authentication library for PAM
22 RhodeCode authentication library for PAM
23 """
23 """
24
24
25 import colander
25 import colander
26 import grp
26 import grp
27 import logging
27 import logging
28 import pam
28 import pam
29 import pwd
29 import pwd
30 import re
30 import re
31 import socket
31 import socket
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class PamAuthnResource(AuthnPluginResourceBase):
52 class PamAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 service = colander.SchemaNode(
57 service = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='login',
59 default='login',
60 description=_('PAM service name to use for authentication.'),
60 description=_('PAM service name to use for authentication.'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('PAM service name'),
62 title=_('PAM service name'),
63 widget='string')
63 widget='string')
64 gecos = colander.SchemaNode(
64 gecos = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 description=_('Regular expression for extracting user name/email etc. '
67 description=_('Regular expression for extracting user name/email etc. '
68 'from Unix userinfo.'),
68 'from Unix userinfo.'),
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 title=_('Gecos Regex'),
70 title=_('Gecos Regex'),
71 widget='string')
71 widget='string')
72
72
73
73
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 # PAM authentication can be slow. Repository operations involve a lot of
75 # PAM authentication can be slow. Repository operations involve a lot of
76 # auth calls. Little caching helps speedup push/pull operations significantly
76 # auth calls. Little caching helps speedup push/pull operations significantly
77 AUTH_CACHE_TTL = 4
77 AUTH_CACHE_TTL = 4
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=PamAuthnResource)
88 context=PamAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=PamAuthnResource)
95 context=PamAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('PAM')
98 return _('PAM')
99
99
100 @hybrid_property
100 @hybrid_property
101 def name(self):
101 def name(self):
102 return "pam"
102 return "pam"
103
103
104 def get_settings_schema(self):
104 def get_settings_schema(self):
105 return PamSettingsSchema()
105 return PamSettingsSchema()
106
106
107 def use_fake_password(self):
107 def use_fake_password(self):
108 return True
108 return True
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 if not username or not password:
111 if not username or not password:
112 log.debug('Empty username or password skipping...')
112 log.debug('Empty username or password skipping...')
113 return None
113 return None
114 _pam = pam.pam()
114 _pam = pam.pam()
115 auth_result = _pam.authenticate(username, password, settings["service"])
115 auth_result = _pam.authenticate(username, password, settings["service"])
116
116
117 if not auth_result:
117 if not auth_result:
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
118 log.error("PAM was unable to authenticate user: %s", username)
119 return None
119 return None
120
120
121 log.debug('Got PAM response %s' % (auth_result, ))
121 log.debug('Got PAM response %s', auth_result)
122
122
123 # old attrs fetched from RhodeCode database
123 # old attrs fetched from RhodeCode database
124 default_email = "%s@%s" % (username, socket.gethostname())
124 default_email = "%s@%s" % (username, socket.gethostname())
125 admin = getattr(userobj, 'admin', False)
125 admin = getattr(userobj, 'admin', False)
126 active = getattr(userobj, 'active', True)
126 active = getattr(userobj, 'active', True)
127 email = getattr(userobj, 'email', '') or default_email
127 email = getattr(userobj, 'email', '') or default_email
128 username = getattr(userobj, 'username', username)
128 username = getattr(userobj, 'username', username)
129 firstname = getattr(userobj, 'firstname', '')
129 firstname = getattr(userobj, 'firstname', '')
130 lastname = getattr(userobj, 'lastname', '')
130 lastname = getattr(userobj, 'lastname', '')
131 extern_type = getattr(userobj, 'extern_type', '')
131 extern_type = getattr(userobj, 'extern_type', '')
132
132
133 user_attrs = {
133 user_attrs = {
134 'username': username,
134 'username': username,
135 'firstname': firstname,
135 'firstname': firstname,
136 'lastname': lastname,
136 'lastname': lastname,
137 'groups': [g.gr_name for g in grp.getgrall()
137 'groups': [g.gr_name for g in grp.getgrall()
138 if username in g.gr_mem],
138 if username in g.gr_mem],
139 'user_group_sync': True,
139 'user_group_sync': True,
140 'email': email,
140 'email': email,
141 'admin': admin,
141 'admin': admin,
142 'active': active,
142 'active': active,
143 'active_from_extern': None,
143 'active_from_extern': None,
144 'extern_name': username,
144 'extern_name': username,
145 'extern_type': extern_type,
145 'extern_type': extern_type,
146 }
146 }
147
147
148 try:
148 try:
149 user_data = pwd.getpwnam(username)
149 user_data = pwd.getpwnam(username)
150 regex = settings["gecos"]
150 regex = settings["gecos"]
151 match = re.search(regex, user_data.pw_gecos)
151 match = re.search(regex, user_data.pw_gecos)
152 if match:
152 if match:
153 user_attrs["firstname"] = match.group('first_name')
153 user_attrs["firstname"] = match.group('first_name')
154 user_attrs["lastname"] = match.group('last_name')
154 user_attrs["lastname"] = match.group('last_name')
155 except Exception:
155 except Exception:
156 log.warning("Cannot extract additional info for PAM user")
156 log.warning("Cannot extract additional info for PAM user")
157 pass
157 pass
158
158
159 log.debug("pamuser: %s", user_attrs)
159 log.debug("pamuser: %s", user_attrs)
160 log.info('user `%s` authenticated correctly' % user_attrs['username'])
160 log.info('user `%s` authenticated correctly', user_attrs['username'])
161 return user_attrs
161 return user_attrs
@@ -1,143 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 RhodeCode authentication plugin for built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28
28
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47
47
48 def includeme(self, config):
48 def includeme(self, config):
49 config.add_authn_plugin(self)
49 config.add_authn_plugin(self)
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 config.add_view(
51 config.add_view(
52 'rhodecode.authentication.views.AuthnPluginViewBase',
52 'rhodecode.authentication.views.AuthnPluginViewBase',
53 attr='settings_get',
53 attr='settings_get',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 request_method='GET',
55 request_method='GET',
56 route_name='auth_home',
56 route_name='auth_home',
57 context=RhodecodeAuthnResource)
57 context=RhodecodeAuthnResource)
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_post',
60 attr='settings_post',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='POST',
62 request_method='POST',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65
65
66 def get_display_name(self):
66 def get_display_name(self):
67 return _('Rhodecode')
67 return _('Rhodecode')
68
68
69 @hybrid_property
69 @hybrid_property
70 def name(self):
70 def name(self):
71 return "rhodecode"
71 return "rhodecode"
72
72
73 def user_activation_state(self):
73 def user_activation_state(self):
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
75 return 'hg.register.auto_activate' in def_user_perms
75 return 'hg.register.auto_activate' in def_user_perms
76
76
77 def allows_authentication_from(
77 def allows_authentication_from(
78 self, user, allows_non_existing_user=True,
78 self, user, allows_non_existing_user=True,
79 allowed_auth_plugins=None, allowed_auth_sources=None):
79 allowed_auth_plugins=None, allowed_auth_sources=None):
80 """
80 """
81 Custom method for this auth that doesn't accept non existing users.
81 Custom method for this auth that doesn't accept non existing users.
82 We know that user exists in our database.
82 We know that user exists in our database.
83 """
83 """
84 allows_non_existing_user = False
84 allows_non_existing_user = False
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 user, allows_non_existing_user=allows_non_existing_user)
86 user, allows_non_existing_user=allows_non_existing_user)
87
87
88 def auth(self, userobj, username, password, settings, **kwargs):
88 def auth(self, userobj, username, password, settings, **kwargs):
89 if not userobj:
89 if not userobj:
90 log.debug('userobj was:%s skipping' % (userobj, ))
90 log.debug('userobj was:%s skipping', userobj)
91 return None
91 return None
92 if userobj.extern_type != self.name:
92 if userobj.extern_type != self.name:
93 log.warning(
93 log.warning(
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`",
95 (userobj, userobj.extern_type, self.name))
95 userobj, userobj.extern_type, self.name)
96 return None
96 return None
97
97
98 user_attrs = {
98 user_attrs = {
99 "username": userobj.username,
99 "username": userobj.username,
100 "firstname": userobj.firstname,
100 "firstname": userobj.firstname,
101 "lastname": userobj.lastname,
101 "lastname": userobj.lastname,
102 "groups": [],
102 "groups": [],
103 'user_group_sync': False,
103 'user_group_sync': False,
104 "email": userobj.email,
104 "email": userobj.email,
105 "admin": userobj.admin,
105 "admin": userobj.admin,
106 "active": userobj.active,
106 "active": userobj.active,
107 "active_from_extern": userobj.active,
107 "active_from_extern": userobj.active,
108 "extern_name": userobj.user_id,
108 "extern_name": userobj.user_id,
109 "extern_type": userobj.extern_type,
109 "extern_type": userobj.extern_type,
110 }
110 }
111
111
112 log.debug("User attributes:%s" % (user_attrs, ))
112 log.debug("User attributes:%s", user_attrs)
113 if userobj.active:
113 if userobj.active:
114 from rhodecode.lib import auth
114 from rhodecode.lib import auth
115 crypto_backend = auth.crypto_backend()
115 crypto_backend = auth.crypto_backend()
116 password_encoded = safe_str(password)
116 password_encoded = safe_str(password)
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
118 password_encoded, userobj.password or '')
118 password_encoded, userobj.password or '')
119
119
120 if password_match and new_hash:
120 if password_match and new_hash:
121 log.debug('user %s properly authenticated, but '
121 log.debug('user %s properly authenticated, but '
122 'requires hash change to bcrypt', userobj)
122 'requires hash change to bcrypt', userobj)
123 # if password match, and we use OLD deprecated hash,
123 # if password match, and we use OLD deprecated hash,
124 # we should migrate this user hash password to the new hash
124 # we should migrate this user hash password to the new hash
125 # we store the new returned by hash_check_with_upgrade function
125 # we store the new returned by hash_check_with_upgrade function
126 user_attrs['_hash_migrate'] = new_hash
126 user_attrs['_hash_migrate'] = new_hash
127
127
128 if userobj.username == User.DEFAULT_USER and userobj.active:
128 if userobj.username == User.DEFAULT_USER and userobj.active:
129 log.info(
129 log.info(
130 'user `%s` authenticated correctly as anonymous user', userobj.username)
130 'user `%s` authenticated correctly as anonymous user', userobj.username)
131 return user_attrs
131 return user_attrs
132
132
133 elif userobj.username == username and password_match:
133 elif userobj.username == username and password_match:
134 log.info('user `%s` authenticated correctly', userobj.username)
134 log.info('user `%s` authenticated correctly', userobj.username)
135 return user_attrs
135 return user_attrs
136 log.warn("user `%s` used a wrong password when "
136 log.warn("user `%s` used a wrong password when "
137 "authenticating on this plugin", userobj.username)
137 "authenticating on this plugin", userobj.username)
138 return None
138 return None
139 else:
139 else:
140 log.warning(
140 log.warning(
141 'user `%s` failed to authenticate via %s, reason: account not '
141 'user `%s` failed to authenticate via %s, reason: account not '
142 'active.', username, self.name)
142 'active.', username, self.name)
143 return None
143 return None
@@ -1,147 +1,147 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-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 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys, Repository
31 from rhodecode.model.db import User, UserApiKeys, Repository
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
47 """
48 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
49 """
49 """
50
50
51 def includeme(self, config):
51 def includeme(self, config):
52 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_view(
54 config.add_view(
55 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 attr='settings_get',
56 attr='settings_get',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 request_method='GET',
58 request_method='GET',
59 route_name='auth_home',
59 route_name='auth_home',
60 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
61 config.add_view(
61 config.add_view(
62 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 attr='settings_post',
63 attr='settings_post',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 request_method='POST',
65 request_method='POST',
66 route_name='auth_home',
66 route_name='auth_home',
67 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
68
68
69 def get_display_name(self):
69 def get_display_name(self):
70 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
71
71
72 @hybrid_property
72 @hybrid_property
73 def name(self):
73 def name(self):
74 return "authtoken"
74 return "authtoken"
75
75
76 def user_activation_state(self):
76 def user_activation_state(self):
77 def_user_perms = User.get_default_user().AuthUser().permissions['global']
77 def_user_perms = User.get_default_user().AuthUser().permissions['global']
78 return 'hg.register.auto_activate' in def_user_perms
78 return 'hg.register.auto_activate' in def_user_perms
79
79
80 def allows_authentication_from(
80 def allows_authentication_from(
81 self, user, allows_non_existing_user=True,
81 self, user, allows_non_existing_user=True,
82 allowed_auth_plugins=None, allowed_auth_sources=None):
82 allowed_auth_plugins=None, allowed_auth_sources=None):
83 """
83 """
84 Custom method for this auth that doesn't accept empty users. And also
84 Custom method for this auth that doesn't accept empty users. And also
85 allows users from all other active plugins to use it and also
85 allows users from all other active plugins to use it and also
86 authenticate against it. But only via vcs mode
86 authenticate against it. But only via vcs mode
87 """
87 """
88 from rhodecode.authentication.base import get_authn_registry
88 from rhodecode.authentication.base import get_authn_registry
89 authn_registry = get_authn_registry()
89 authn_registry = get_authn_registry()
90
90
91 active_plugins = set(
91 active_plugins = set(
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 active_plugins.discard(self.name)
93 active_plugins.discard(self.name)
94
94
95 allowed_auth_plugins = [self.name] + list(active_plugins)
95 allowed_auth_plugins = [self.name] + list(active_plugins)
96 # only for vcs operations
96 # only for vcs operations
97 allowed_auth_sources = [VCS_TYPE]
97 allowed_auth_sources = [VCS_TYPE]
98
98
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 user, allows_non_existing_user=False,
100 user, allows_non_existing_user=False,
101 allowed_auth_plugins=allowed_auth_plugins,
101 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_sources=allowed_auth_sources)
102 allowed_auth_sources=allowed_auth_sources)
103
103
104 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
105 if not userobj:
106 log.debug('userobj was:%s skipping' % (userobj, ))
106 log.debug('userobj was:%s skipping', userobj)
107 return None
107 return None
108
108
109 user_attrs = {
109 user_attrs = {
110 "username": userobj.username,
110 "username": userobj.username,
111 "firstname": userobj.firstname,
111 "firstname": userobj.firstname,
112 "lastname": userobj.lastname,
112 "lastname": userobj.lastname,
113 "groups": [],
113 "groups": [],
114 'user_group_sync': False,
114 'user_group_sync': False,
115 "email": userobj.email,
115 "email": userobj.email,
116 "admin": userobj.admin,
116 "admin": userobj.admin,
117 "active": userobj.active,
117 "active": userobj.active,
118 "active_from_extern": userobj.active,
118 "active_from_extern": userobj.active,
119 "extern_name": userobj.user_id,
119 "extern_name": userobj.user_id,
120 "extern_type": userobj.extern_type,
120 "extern_type": userobj.extern_type,
121 }
121 }
122
122
123 log.debug('Authenticating user with args %s', user_attrs)
123 log.debug('Authenticating user with args %s', user_attrs)
124 if userobj.active:
124 if userobj.active:
125 # calling context repo for token scopes
125 # calling context repo for token scopes
126 scope_repo_id = None
126 scope_repo_id = None
127 if self.acl_repo_name:
127 if self.acl_repo_name:
128 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 repo = Repository.get_by_repo_name(self.acl_repo_name)
129 scope_repo_id = repo.repo_id if repo else None
129 scope_repo_id = repo.repo_id if repo else None
130
130
131 token_match = userobj.authenticate_by_token(
131 token_match = userobj.authenticate_by_token(
132 password, roles=[UserApiKeys.ROLE_VCS],
132 password, roles=[UserApiKeys.ROLE_VCS],
133 scope_repo_id=scope_repo_id)
133 scope_repo_id=scope_repo_id)
134
134
135 if userobj.username == username and token_match:
135 if userobj.username == username and token_match:
136 log.info(
136 log.info(
137 'user `%s` successfully authenticated via %s',
137 'user `%s` successfully authenticated via %s',
138 user_attrs['username'], self.name)
138 user_attrs['username'], self.name)
139 return user_attrs
139 return user_attrs
140 log.warn(
140 log.warn(
141 'user `%s` failed to authenticate via %s, reason: bad or '
141 'user `%s` failed to authenticate via %s, reason: bad or '
142 'inactive token.', username, self.name)
142 'inactive token.', username, self.name)
143 else:
143 else:
144 log.warning(
144 log.warning(
145 'user `%s` failed to authenticate via %s, reason: account not '
145 'user `%s` failed to authenticate via %s, reason: account not '
146 'active.', username, self.name)
146 'active.', username, self.name)
147 return None
147 return None
@@ -1,356 +1,356 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 collections
19 import collections
20 import logging
20 import logging
21 import datetime
21 import datetime
22
22
23 from rhodecode.translation import lazy_ugettext
23 from rhodecode.translation import lazy_ugettext
24 from rhodecode.model.db import User, Repository, Session
24 from rhodecode.model.db import User, Repository, Session
25 from rhodecode.events.base import RhodeCodeIntegrationEvent
25 from rhodecode.events.base import RhodeCodeIntegrationEvent
26 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30
30
31 def _commits_as_dict(event, commit_ids, repos):
31 def _commits_as_dict(event, commit_ids, repos):
32 """
32 """
33 Helper function to serialize commit_ids
33 Helper function to serialize commit_ids
34
34
35 :param event: class calling this method
35 :param event: class calling this method
36 :param commit_ids: commits to get
36 :param commit_ids: commits to get
37 :param repos: list of repos to check
37 :param repos: list of repos to check
38 """
38 """
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.lib.helpers import (
40 from rhodecode.lib.helpers import (
41 urlify_commit_message, process_patterns, chop_at_smart)
41 urlify_commit_message, process_patterns, chop_at_smart)
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43
43
44 if not repos:
44 if not repos:
45 raise Exception('no repo defined')
45 raise Exception('no repo defined')
46
46
47 if not isinstance(repos, (tuple, list)):
47 if not isinstance(repos, (tuple, list)):
48 repos = [repos]
48 repos = [repos]
49
49
50 if not commit_ids:
50 if not commit_ids:
51 return []
51 return []
52
52
53 needed_commits = list(commit_ids)
53 needed_commits = list(commit_ids)
54
54
55 commits = []
55 commits = []
56 reviewers = []
56 reviewers = []
57 for repo in repos:
57 for repo in repos:
58 if not needed_commits:
58 if not needed_commits:
59 return commits # return early if we have the commits we need
59 return commits # return early if we have the commits we need
60
60
61 vcs_repo = repo.scm_instance(cache=False)
61 vcs_repo = repo.scm_instance(cache=False)
62
62
63 try:
63 try:
64 # use copy of needed_commits since we modify it while iterating
64 # use copy of needed_commits since we modify it while iterating
65 for commit_id in list(needed_commits):
65 for commit_id in list(needed_commits):
66 if commit_id.startswith('tag=>'):
66 if commit_id.startswith('tag=>'):
67 raw_id = commit_id[5:]
67 raw_id = commit_id[5:]
68 cs_data = {
68 cs_data = {
69 'raw_id': commit_id, 'short_id': commit_id,
69 'raw_id': commit_id, 'short_id': commit_id,
70 'branch': None,
70 'branch': None,
71 'git_ref_change': 'tag_add',
71 'git_ref_change': 'tag_add',
72 'message': 'Added new tag {}'.format(raw_id),
72 'message': 'Added new tag {}'.format(raw_id),
73 'author': event.actor.full_contact,
73 'author': event.actor.full_contact,
74 'date': datetime.datetime.now(),
74 'date': datetime.datetime.now(),
75 'refs': {
75 'refs': {
76 'branches': [],
76 'branches': [],
77 'bookmarks': [],
77 'bookmarks': [],
78 'tags': []
78 'tags': []
79 }
79 }
80 }
80 }
81 commits.append(cs_data)
81 commits.append(cs_data)
82
82
83 elif commit_id.startswith('delete_branch=>'):
83 elif commit_id.startswith('delete_branch=>'):
84 raw_id = commit_id[15:]
84 raw_id = commit_id[15:]
85 cs_data = {
85 cs_data = {
86 'raw_id': commit_id, 'short_id': commit_id,
86 'raw_id': commit_id, 'short_id': commit_id,
87 'branch': None,
87 'branch': None,
88 'git_ref_change': 'branch_delete',
88 'git_ref_change': 'branch_delete',
89 'message': 'Deleted branch {}'.format(raw_id),
89 'message': 'Deleted branch {}'.format(raw_id),
90 'author': event.actor.full_contact,
90 'author': event.actor.full_contact,
91 'date': datetime.datetime.now(),
91 'date': datetime.datetime.now(),
92 'refs': {
92 'refs': {
93 'branches': [],
93 'branches': [],
94 'bookmarks': [],
94 'bookmarks': [],
95 'tags': []
95 'tags': []
96 }
96 }
97 }
97 }
98 commits.append(cs_data)
98 commits.append(cs_data)
99
99
100 else:
100 else:
101 try:
101 try:
102 cs = vcs_repo.get_changeset(commit_id)
102 cs = vcs_repo.get_changeset(commit_id)
103 except CommitDoesNotExistError:
103 except CommitDoesNotExistError:
104 continue # maybe its in next repo
104 continue # maybe its in next repo
105
105
106 cs_data = cs.__json__()
106 cs_data = cs.__json__()
107 cs_data['refs'] = cs._get_refs()
107 cs_data['refs'] = cs._get_refs()
108
108
109 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
109 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
110 cs_data['reviewers'] = reviewers
110 cs_data['reviewers'] = reviewers
111 cs_data['url'] = RepoModel().get_commit_url(
111 cs_data['url'] = RepoModel().get_commit_url(
112 repo, cs_data['raw_id'], request=event.request)
112 repo, cs_data['raw_id'], request=event.request)
113 cs_data['permalink_url'] = RepoModel().get_commit_url(
113 cs_data['permalink_url'] = RepoModel().get_commit_url(
114 repo, cs_data['raw_id'], request=event.request,
114 repo, cs_data['raw_id'], request=event.request,
115 permalink=True)
115 permalink=True)
116 urlified_message, issues_data = process_patterns(
116 urlified_message, issues_data = process_patterns(
117 cs_data['message'], repo.repo_name)
117 cs_data['message'], repo.repo_name)
118 cs_data['issues'] = issues_data
118 cs_data['issues'] = issues_data
119 cs_data['message_html'] = urlify_commit_message(
119 cs_data['message_html'] = urlify_commit_message(
120 cs_data['message'], repo.repo_name)
120 cs_data['message'], repo.repo_name)
121 cs_data['message_html_title'] = chop_at_smart(
121 cs_data['message_html_title'] = chop_at_smart(
122 cs_data['message'], '\n', suffix_if_chopped='...')
122 cs_data['message'], '\n', suffix_if_chopped='...')
123 commits.append(cs_data)
123 commits.append(cs_data)
124
124
125 needed_commits.remove(commit_id)
125 needed_commits.remove(commit_id)
126
126
127 except Exception:
127 except Exception:
128 log.exception('Failed to extract commits data')
128 log.exception('Failed to extract commits data')
129 # we don't send any commits when crash happens, only full list
129 # we don't send any commits when crash happens, only full list
130 # matters we short circuit then.
130 # matters we short circuit then.
131 return []
131 return []
132
132
133 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
133 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
134 if missing_commits:
134 if missing_commits:
135 log.error('Inconsistent repository state. '
135 log.error('Inconsistent repository state. '
136 'Missing commits: %s' % ', '.join(missing_commits))
136 'Missing commits: %s', ', '.join(missing_commits))
137
137
138 return commits
138 return commits
139
139
140
140
141 def _issues_as_dict(commits):
141 def _issues_as_dict(commits):
142 """ Helper function to serialize issues from commits """
142 """ Helper function to serialize issues from commits """
143 issues = {}
143 issues = {}
144 for commit in commits:
144 for commit in commits:
145 for issue in commit['issues']:
145 for issue in commit['issues']:
146 issues[issue['id']] = issue
146 issues[issue['id']] = issue
147 return issues
147 return issues
148
148
149
149
150 class RepoEvent(RhodeCodeIntegrationEvent):
150 class RepoEvent(RhodeCodeIntegrationEvent):
151 """
151 """
152 Base class for events acting on a repository.
152 Base class for events acting on a repository.
153
153
154 :param repo: a :class:`Repository` instance
154 :param repo: a :class:`Repository` instance
155 """
155 """
156
156
157 def __init__(self, repo):
157 def __init__(self, repo):
158 super(RepoEvent, self).__init__()
158 super(RepoEvent, self).__init__()
159 self.repo = repo
159 self.repo = repo
160
160
161 def as_dict(self):
161 def as_dict(self):
162 from rhodecode.model.repo import RepoModel
162 from rhodecode.model.repo import RepoModel
163 data = super(RepoEvent, self).as_dict()
163 data = super(RepoEvent, self).as_dict()
164
164
165 extra_fields = collections.OrderedDict()
165 extra_fields = collections.OrderedDict()
166 for field in self.repo.extra_fields:
166 for field in self.repo.extra_fields:
167 extra_fields[field.field_key] = field.field_value
167 extra_fields[field.field_key] = field.field_value
168
168
169 data.update({
169 data.update({
170 'repo': {
170 'repo': {
171 'repo_id': self.repo.repo_id,
171 'repo_id': self.repo.repo_id,
172 'repo_name': self.repo.repo_name,
172 'repo_name': self.repo.repo_name,
173 'repo_type': self.repo.repo_type,
173 'repo_type': self.repo.repo_type,
174 'url': RepoModel().get_url(
174 'url': RepoModel().get_url(
175 self.repo, request=self.request),
175 self.repo, request=self.request),
176 'permalink_url': RepoModel().get_url(
176 'permalink_url': RepoModel().get_url(
177 self.repo, request=self.request, permalink=True),
177 self.repo, request=self.request, permalink=True),
178 'extra_fields': extra_fields
178 'extra_fields': extra_fields
179 }
179 }
180 })
180 })
181 return data
181 return data
182
182
183
183
184 class RepoPreCreateEvent(RepoEvent):
184 class RepoPreCreateEvent(RepoEvent):
185 """
185 """
186 An instance of this class is emitted as an :term:`event` before a repo is
186 An instance of this class is emitted as an :term:`event` before a repo is
187 created.
187 created.
188 """
188 """
189 name = 'repo-pre-create'
189 name = 'repo-pre-create'
190 display_name = lazy_ugettext('repository pre create')
190 display_name = lazy_ugettext('repository pre create')
191
191
192
192
193 class RepoCreateEvent(RepoEvent):
193 class RepoCreateEvent(RepoEvent):
194 """
194 """
195 An instance of this class is emitted as an :term:`event` whenever a repo is
195 An instance of this class is emitted as an :term:`event` whenever a repo is
196 created.
196 created.
197 """
197 """
198 name = 'repo-create'
198 name = 'repo-create'
199 display_name = lazy_ugettext('repository created')
199 display_name = lazy_ugettext('repository created')
200
200
201
201
202 class RepoPreDeleteEvent(RepoEvent):
202 class RepoPreDeleteEvent(RepoEvent):
203 """
203 """
204 An instance of this class is emitted as an :term:`event` whenever a repo is
204 An instance of this class is emitted as an :term:`event` whenever a repo is
205 created.
205 created.
206 """
206 """
207 name = 'repo-pre-delete'
207 name = 'repo-pre-delete'
208 display_name = lazy_ugettext('repository pre delete')
208 display_name = lazy_ugettext('repository pre delete')
209
209
210
210
211 class RepoDeleteEvent(RepoEvent):
211 class RepoDeleteEvent(RepoEvent):
212 """
212 """
213 An instance of this class is emitted as an :term:`event` whenever a repo is
213 An instance of this class is emitted as an :term:`event` whenever a repo is
214 created.
214 created.
215 """
215 """
216 name = 'repo-delete'
216 name = 'repo-delete'
217 display_name = lazy_ugettext('repository deleted')
217 display_name = lazy_ugettext('repository deleted')
218
218
219
219
220 class RepoVCSEvent(RepoEvent):
220 class RepoVCSEvent(RepoEvent):
221 """
221 """
222 Base class for events triggered by the VCS
222 Base class for events triggered by the VCS
223 """
223 """
224 def __init__(self, repo_name, extras):
224 def __init__(self, repo_name, extras):
225 self.repo = Repository.get_by_repo_name(repo_name)
225 self.repo = Repository.get_by_repo_name(repo_name)
226 if not self.repo:
226 if not self.repo:
227 raise Exception('repo by this name %s does not exist' % repo_name)
227 raise Exception('repo by this name %s does not exist' % repo_name)
228 self.extras = extras
228 self.extras = extras
229 super(RepoVCSEvent, self).__init__(self.repo)
229 super(RepoVCSEvent, self).__init__(self.repo)
230
230
231 @property
231 @property
232 def actor(self):
232 def actor(self):
233 if self.extras.get('username'):
233 if self.extras.get('username'):
234 return User.get_by_username(self.extras['username'])
234 return User.get_by_username(self.extras['username'])
235
235
236 @property
236 @property
237 def actor_ip(self):
237 def actor_ip(self):
238 if self.extras.get('ip'):
238 if self.extras.get('ip'):
239 return self.extras['ip']
239 return self.extras['ip']
240
240
241 @property
241 @property
242 def server_url(self):
242 def server_url(self):
243 if self.extras.get('server_url'):
243 if self.extras.get('server_url'):
244 return self.extras['server_url']
244 return self.extras['server_url']
245
245
246 @property
246 @property
247 def request(self):
247 def request(self):
248 return self.extras.get('request') or self.get_request()
248 return self.extras.get('request') or self.get_request()
249
249
250
250
251 class RepoPrePullEvent(RepoVCSEvent):
251 class RepoPrePullEvent(RepoVCSEvent):
252 """
252 """
253 An instance of this class is emitted as an :term:`event` before commits
253 An instance of this class is emitted as an :term:`event` before commits
254 are pulled from a repo.
254 are pulled from a repo.
255 """
255 """
256 name = 'repo-pre-pull'
256 name = 'repo-pre-pull'
257 display_name = lazy_ugettext('repository pre pull')
257 display_name = lazy_ugettext('repository pre pull')
258
258
259
259
260 class RepoPullEvent(RepoVCSEvent):
260 class RepoPullEvent(RepoVCSEvent):
261 """
261 """
262 An instance of this class is emitted as an :term:`event` after commits
262 An instance of this class is emitted as an :term:`event` after commits
263 are pulled from a repo.
263 are pulled from a repo.
264 """
264 """
265 name = 'repo-pull'
265 name = 'repo-pull'
266 display_name = lazy_ugettext('repository pull')
266 display_name = lazy_ugettext('repository pull')
267
267
268
268
269 class RepoPrePushEvent(RepoVCSEvent):
269 class RepoPrePushEvent(RepoVCSEvent):
270 """
270 """
271 An instance of this class is emitted as an :term:`event` before commits
271 An instance of this class is emitted as an :term:`event` before commits
272 are pushed to a repo.
272 are pushed to a repo.
273 """
273 """
274 name = 'repo-pre-push'
274 name = 'repo-pre-push'
275 display_name = lazy_ugettext('repository pre push')
275 display_name = lazy_ugettext('repository pre push')
276
276
277
277
278 class RepoPushEvent(RepoVCSEvent):
278 class RepoPushEvent(RepoVCSEvent):
279 """
279 """
280 An instance of this class is emitted as an :term:`event` after commits
280 An instance of this class is emitted as an :term:`event` after commits
281 are pushed to a repo.
281 are pushed to a repo.
282
282
283 :param extras: (optional) dict of data from proxied VCS actions
283 :param extras: (optional) dict of data from proxied VCS actions
284 """
284 """
285 name = 'repo-push'
285 name = 'repo-push'
286 display_name = lazy_ugettext('repository push')
286 display_name = lazy_ugettext('repository push')
287
287
288 def __init__(self, repo_name, pushed_commit_ids, extras):
288 def __init__(self, repo_name, pushed_commit_ids, extras):
289 super(RepoPushEvent, self).__init__(repo_name, extras)
289 super(RepoPushEvent, self).__init__(repo_name, extras)
290 self.pushed_commit_ids = pushed_commit_ids
290 self.pushed_commit_ids = pushed_commit_ids
291 self.new_refs = extras.new_refs
291 self.new_refs = extras.new_refs
292
292
293 def as_dict(self):
293 def as_dict(self):
294 data = super(RepoPushEvent, self).as_dict()
294 data = super(RepoPushEvent, self).as_dict()
295
295
296 def branch_url(branch_name):
296 def branch_url(branch_name):
297 return '{}/changelog?branch={}'.format(
297 return '{}/changelog?branch={}'.format(
298 data['repo']['url'], branch_name)
298 data['repo']['url'], branch_name)
299
299
300 def tag_url(tag_name):
300 def tag_url(tag_name):
301 return '{}/files/{}/'.format(
301 return '{}/files/{}/'.format(
302 data['repo']['url'], tag_name)
302 data['repo']['url'], tag_name)
303
303
304 commits = _commits_as_dict(
304 commits = _commits_as_dict(
305 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
305 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
306
306
307 last_branch = None
307 last_branch = None
308 for commit in reversed(commits):
308 for commit in reversed(commits):
309 commit['branch'] = commit['branch'] or last_branch
309 commit['branch'] = commit['branch'] or last_branch
310 last_branch = commit['branch']
310 last_branch = commit['branch']
311 issues = _issues_as_dict(commits)
311 issues = _issues_as_dict(commits)
312
312
313 branches = set()
313 branches = set()
314 tags = set()
314 tags = set()
315 for commit in commits:
315 for commit in commits:
316 if commit['refs']['tags']:
316 if commit['refs']['tags']:
317 for tag in commit['refs']['tags']:
317 for tag in commit['refs']['tags']:
318 tags.add(tag)
318 tags.add(tag)
319 if commit['branch']:
319 if commit['branch']:
320 branches.add(commit['branch'])
320 branches.add(commit['branch'])
321
321
322 # maybe we have branches in new_refs ?
322 # maybe we have branches in new_refs ?
323 try:
323 try:
324 branches = branches.union(set(self.new_refs['branches']))
324 branches = branches.union(set(self.new_refs['branches']))
325 except Exception:
325 except Exception:
326 pass
326 pass
327
327
328 branches = [
328 branches = [
329 {
329 {
330 'name': branch,
330 'name': branch,
331 'url': branch_url(branch)
331 'url': branch_url(branch)
332 }
332 }
333 for branch in branches
333 for branch in branches
334 ]
334 ]
335
335
336 # maybe we have branches in new_refs ?
336 # maybe we have branches in new_refs ?
337 try:
337 try:
338 tags = tags.union(set(self.new_refs['tags']))
338 tags = tags.union(set(self.new_refs['tags']))
339 except Exception:
339 except Exception:
340 pass
340 pass
341
341
342 tags = [
342 tags = [
343 {
343 {
344 'name': tag,
344 'name': tag,
345 'url': tag_url(tag)
345 'url': tag_url(tag)
346 }
346 }
347 for tag in tags
347 for tag in tags
348 ]
348 ]
349
349
350 data['push'] = {
350 data['push'] = {
351 'commits': commits,
351 'commits': commits,
352 'issues': issues,
352 'issues': issues,
353 'branches': branches,
353 'branches': branches,
354 'tags': tags,
354 'tags': tags,
355 }
355 }
356 return data
356 return data
@@ -1,74 +1,74 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 sys
20 import sys
21 import logging
21 import logging
22
22
23 from rhodecode.integrations.registry import IntegrationTypeRegistry
23 from rhodecode.integrations.registry import IntegrationTypeRegistry
24 from rhodecode.integrations.types import webhook, slack, hipchat, email, base
24 from rhodecode.integrations.types import webhook, slack, hipchat, email, base
25 from rhodecode.lib.exc_tracking import store_exception
25 from rhodecode.lib.exc_tracking import store_exception
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 # TODO: dan: This is currently global until we figure out what to do about
30 # TODO: dan: This is currently global until we figure out what to do about
31 # VCS's not having a pyramid context - move it to pyramid app configuration
31 # VCS's not having a pyramid context - move it to pyramid app configuration
32 # includeme level later to allow per instance integration setup
32 # includeme level later to allow per instance integration setup
33 integration_type_registry = IntegrationTypeRegistry()
33 integration_type_registry = IntegrationTypeRegistry()
34
34
35 integration_type_registry.register_integration_type(
35 integration_type_registry.register_integration_type(
36 webhook.WebhookIntegrationType)
36 webhook.WebhookIntegrationType)
37 integration_type_registry.register_integration_type(
37 integration_type_registry.register_integration_type(
38 slack.SlackIntegrationType)
38 slack.SlackIntegrationType)
39 integration_type_registry.register_integration_type(
39 integration_type_registry.register_integration_type(
40 hipchat.HipchatIntegrationType)
40 hipchat.HipchatIntegrationType)
41 integration_type_registry.register_integration_type(
41 integration_type_registry.register_integration_type(
42 email.EmailIntegrationType)
42 email.EmailIntegrationType)
43
43
44
44
45 # dummy EE integration to show users what we have in EE edition
45 # dummy EE integration to show users what we have in EE edition
46 integration_type_registry.register_integration_type(
46 integration_type_registry.register_integration_type(
47 base.EEIntegration('Jira Issues integration', 'jira'))
47 base.EEIntegration('Jira Issues integration', 'jira'))
48 integration_type_registry.register_integration_type(
48 integration_type_registry.register_integration_type(
49 base.EEIntegration('Redmine Tracker integration', 'redmine'))
49 base.EEIntegration('Redmine Tracker integration', 'redmine'))
50 integration_type_registry.register_integration_type(
50 integration_type_registry.register_integration_type(
51 base.EEIntegration('Jenkins CI integration', 'jenkins'))
51 base.EEIntegration('Jenkins CI integration', 'jenkins'))
52
52
53
53
54 def integrations_event_handler(event):
54 def integrations_event_handler(event):
55 """
55 """
56 Takes an event and passes it to all enabled integrations
56 Takes an event and passes it to all enabled integrations
57 """
57 """
58 from rhodecode.model.integration import IntegrationModel
58 from rhodecode.model.integration import IntegrationModel
59
59
60 integration_model = IntegrationModel()
60 integration_model = IntegrationModel()
61 integrations = integration_model.get_for_event(event)
61 integrations = integration_model.get_for_event(event)
62 for integration in integrations:
62 for integration in integrations:
63 try:
63 try:
64 integration_model.send_event(integration, event)
64 integration_model.send_event(integration, event)
65 except Exception:
65 except Exception:
66 exc_info = sys.exc_info()
66 exc_info = sys.exc_info()
67 store_exception(id(exc_info), exc_info)
67 store_exception(id(exc_info), exc_info)
68 log.exception(
68 log.exception(
69 'failure occurred when sending event %s to integration %s' % (
69 'failure occurred when sending event %s to integration %s',
70 event, integration))
70 event, integration)
71
71
72
72
73 def includeme(config):
73 def includeme(config):
74 config.include('rhodecode.integrations.routes')
74 config.include('rhodecode.integrations.routes')
@@ -1,38 +1,37 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012-2018 RhodeCode GmbH
2 # Copyright (C) 2012-2018 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import logging
20 import logging
21 import collections
21 import collections
22
22
23 log = logging.getLogger(__name__)
23 log = logging.getLogger(__name__)
24
24
25
25
26 class IntegrationTypeRegistry(collections.OrderedDict):
26 class IntegrationTypeRegistry(collections.OrderedDict):
27 """
27 """
28 Registry Class to hold IntegrationTypes
28 Registry Class to hold IntegrationTypes
29 """
29 """
30 def register_integration_type(self, IntegrationType):
30 def register_integration_type(self, IntegrationType):
31 key = IntegrationType.key
31 key = IntegrationType.key
32 if key in self:
32 if key in self:
33 log.debug(
33 log.debug(
34 'Overriding existing integration type %s (%s) with %s' % (
34 'Overriding existing integration type %s (%s) with %s',
35 self[key], key, IntegrationType))
35 self[key], key, IntegrationType)
36
36
37 self[key] = IntegrationType
37 self[key] = IntegrationType
38
@@ -1,253 +1,253 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import deform
22 import deform
23 import logging
23 import logging
24 import requests
24 import requests
25 import colander
25 import colander
26 import textwrap
26 import textwrap
27 from mako.template import Template
27 from mako.template import Template
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 from rhodecode.lib.colander_utils import strip_whitespace
32 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.integrations.types.base import (
33 from rhodecode.integrations.types.base import (
34 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback)
34 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback)
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class HipchatSettingsSchema(colander.Schema):
39 class HipchatSettingsSchema(colander.Schema):
40 color_choices = [
40 color_choices = [
41 ('yellow', _('Yellow')),
41 ('yellow', _('Yellow')),
42 ('red', _('Red')),
42 ('red', _('Red')),
43 ('green', _('Green')),
43 ('green', _('Green')),
44 ('purple', _('Purple')),
44 ('purple', _('Purple')),
45 ('gray', _('Gray')),
45 ('gray', _('Gray')),
46 ]
46 ]
47
47
48 server_url = colander.SchemaNode(
48 server_url = colander.SchemaNode(
49 colander.String(),
49 colander.String(),
50 title=_('Hipchat server URL'),
50 title=_('Hipchat server URL'),
51 description=_('Hipchat integration url.'),
51 description=_('Hipchat integration url.'),
52 default='',
52 default='',
53 preparer=strip_whitespace,
53 preparer=strip_whitespace,
54 validator=colander.url,
54 validator=colander.url,
55 widget=deform.widget.TextInputWidget(
55 widget=deform.widget.TextInputWidget(
56 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
56 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
57 ),
57 ),
58 )
58 )
59 notify = colander.SchemaNode(
59 notify = colander.SchemaNode(
60 colander.Bool(),
60 colander.Bool(),
61 title=_('Notify'),
61 title=_('Notify'),
62 description=_('Make a notification to the users in room.'),
62 description=_('Make a notification to the users in room.'),
63 missing=False,
63 missing=False,
64 default=False,
64 default=False,
65 )
65 )
66 color = colander.SchemaNode(
66 color = colander.SchemaNode(
67 colander.String(),
67 colander.String(),
68 title=_('Color'),
68 title=_('Color'),
69 description=_('Background color of message.'),
69 description=_('Background color of message.'),
70 missing='',
70 missing='',
71 validator=colander.OneOf([x[0] for x in color_choices]),
71 validator=colander.OneOf([x[0] for x in color_choices]),
72 widget=deform.widget.Select2Widget(
72 widget=deform.widget.Select2Widget(
73 values=color_choices,
73 values=color_choices,
74 ),
74 ),
75 )
75 )
76
76
77
77
78 repo_push_template = Template('''
78 repo_push_template = Template('''
79 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
79 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
80 <br>
80 <br>
81 <ul>
81 <ul>
82 %for branch, branch_commits in branches_commits.items():
82 %for branch, branch_commits in branches_commits.items():
83 <li>
83 <li>
84 % if branch:
84 % if branch:
85 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
85 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
86 % else:
86 % else:
87 to trunk
87 to trunk
88 % endif
88 % endif
89 <ul>
89 <ul>
90 % for commit in branch_commits['commits']:
90 % for commit in branch_commits['commits']:
91 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
91 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
92 % endfor
92 % endfor
93 </ul>
93 </ul>
94 </li>
94 </li>
95 %endfor
95 %endfor
96 ''')
96 ''')
97
97
98
98
99 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
99 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
100 key = 'hipchat'
100 key = 'hipchat'
101 display_name = _('Hipchat')
101 display_name = _('Hipchat')
102 description = _('Send events such as repo pushes and pull requests to '
102 description = _('Send events such as repo pushes and pull requests to '
103 'your hipchat channel.')
103 'your hipchat channel.')
104
104
105 @classmethod
105 @classmethod
106 def icon(cls):
106 def icon(cls):
107 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
107 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
108
108
109 valid_events = [
109 valid_events = [
110 events.PullRequestCloseEvent,
110 events.PullRequestCloseEvent,
111 events.PullRequestMergeEvent,
111 events.PullRequestMergeEvent,
112 events.PullRequestUpdateEvent,
112 events.PullRequestUpdateEvent,
113 events.PullRequestCommentEvent,
113 events.PullRequestCommentEvent,
114 events.PullRequestReviewEvent,
114 events.PullRequestReviewEvent,
115 events.PullRequestCreateEvent,
115 events.PullRequestCreateEvent,
116 events.RepoPushEvent,
116 events.RepoPushEvent,
117 events.RepoCreateEvent,
117 events.RepoCreateEvent,
118 ]
118 ]
119
119
120 def send_event(self, event):
120 def send_event(self, event):
121 if event.__class__ not in self.valid_events:
121 if event.__class__ not in self.valid_events:
122 log.debug('event not valid: %r' % event)
122 log.debug('event not valid: %r', event)
123 return
123 return
124
124
125 if event.name not in self.settings['events']:
125 if event.name not in self.settings['events']:
126 log.debug('event ignored: %r' % event)
126 log.debug('event ignored: %r', event)
127 return
127 return
128
128
129 data = event.as_dict()
129 data = event.as_dict()
130
130
131 text = '<b>%s<b> caused a <b>%s</b> event' % (
131 text = '<b>%s<b> caused a <b>%s</b> event' % (
132 data['actor']['username'], event.name)
132 data['actor']['username'], event.name)
133
133
134 log.debug('handling hipchat event for %s' % event.name)
134 log.debug('handling hipchat event for %s', event.name)
135
135
136 if isinstance(event, events.PullRequestCommentEvent):
136 if isinstance(event, events.PullRequestCommentEvent):
137 text = self.format_pull_request_comment_event(event, data)
137 text = self.format_pull_request_comment_event(event, data)
138 elif isinstance(event, events.PullRequestReviewEvent):
138 elif isinstance(event, events.PullRequestReviewEvent):
139 text = self.format_pull_request_review_event(event, data)
139 text = self.format_pull_request_review_event(event, data)
140 elif isinstance(event, events.PullRequestEvent):
140 elif isinstance(event, events.PullRequestEvent):
141 text = self.format_pull_request_event(event, data)
141 text = self.format_pull_request_event(event, data)
142 elif isinstance(event, events.RepoPushEvent):
142 elif isinstance(event, events.RepoPushEvent):
143 text = self.format_repo_push_event(data)
143 text = self.format_repo_push_event(data)
144 elif isinstance(event, events.RepoCreateEvent):
144 elif isinstance(event, events.RepoCreateEvent):
145 text = self.format_repo_create_event(data)
145 text = self.format_repo_create_event(data)
146 else:
146 else:
147 log.error('unhandled event type: %r' % event)
147 log.error('unhandled event type: %r', event)
148
148
149 run_task(post_text_to_hipchat, self.settings, text)
149 run_task(post_text_to_hipchat, self.settings, text)
150
150
151 def settings_schema(self):
151 def settings_schema(self):
152 schema = HipchatSettingsSchema()
152 schema = HipchatSettingsSchema()
153 schema.add(colander.SchemaNode(
153 schema.add(colander.SchemaNode(
154 colander.Set(),
154 colander.Set(),
155 widget=deform.widget.CheckboxChoiceWidget(
155 widget=deform.widget.CheckboxChoiceWidget(
156 values=sorted(
156 values=sorted(
157 [(e.name, e.display_name) for e in self.valid_events]
157 [(e.name, e.display_name) for e in self.valid_events]
158 )
158 )
159 ),
159 ),
160 description="Events activated for this integration",
160 description="Events activated for this integration",
161 name='events'
161 name='events'
162 ))
162 ))
163
163
164 return schema
164 return schema
165
165
166 def format_pull_request_comment_event(self, event, data):
166 def format_pull_request_comment_event(self, event, data):
167 comment_text = data['comment']['text']
167 comment_text = data['comment']['text']
168 if len(comment_text) > 200:
168 if len(comment_text) > 200:
169 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
169 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
170 comment_text=h.html_escape(comment_text[:200]),
170 comment_text=h.html_escape(comment_text[:200]),
171 comment_url=data['comment']['url'],
171 comment_url=data['comment']['url'],
172 )
172 )
173
173
174 comment_status = ''
174 comment_status = ''
175 if data['comment']['status']:
175 if data['comment']['status']:
176 comment_status = '[{}]: '.format(data['comment']['status'])
176 comment_status = '[{}]: '.format(data['comment']['status'])
177
177
178 return (textwrap.dedent(
178 return (textwrap.dedent(
179 '''
179 '''
180 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
180 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
181 >>> {comment_status}{comment_text}
181 >>> {comment_status}{comment_text}
182 ''').format(
182 ''').format(
183 comment_status=comment_status,
183 comment_status=comment_status,
184 user=data['actor']['username'],
184 user=data['actor']['username'],
185 number=data['pullrequest']['pull_request_id'],
185 number=data['pullrequest']['pull_request_id'],
186 pr_url=data['pullrequest']['url'],
186 pr_url=data['pullrequest']['url'],
187 pr_status=data['pullrequest']['status'],
187 pr_status=data['pullrequest']['status'],
188 pr_title=h.html_escape(data['pullrequest']['title']),
188 pr_title=h.html_escape(data['pullrequest']['title']),
189 comment_text=h.html_escape(comment_text)
189 comment_text=h.html_escape(comment_text)
190 )
190 )
191 )
191 )
192
192
193 def format_pull_request_review_event(self, event, data):
193 def format_pull_request_review_event(self, event, data):
194 return (textwrap.dedent(
194 return (textwrap.dedent(
195 '''
195 '''
196 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
196 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
197 ''').format(
197 ''').format(
198 user=data['actor']['username'],
198 user=data['actor']['username'],
199 number=data['pullrequest']['pull_request_id'],
199 number=data['pullrequest']['pull_request_id'],
200 pr_url=data['pullrequest']['url'],
200 pr_url=data['pullrequest']['url'],
201 pr_status=data['pullrequest']['status'],
201 pr_status=data['pullrequest']['status'],
202 pr_title=h.html_escape(data['pullrequest']['title']),
202 pr_title=h.html_escape(data['pullrequest']['title']),
203 )
203 )
204 )
204 )
205
205
206 def format_pull_request_event(self, event, data):
206 def format_pull_request_event(self, event, data):
207 action = {
207 action = {
208 events.PullRequestCloseEvent: 'closed',
208 events.PullRequestCloseEvent: 'closed',
209 events.PullRequestMergeEvent: 'merged',
209 events.PullRequestMergeEvent: 'merged',
210 events.PullRequestUpdateEvent: 'updated',
210 events.PullRequestUpdateEvent: 'updated',
211 events.PullRequestCreateEvent: 'created',
211 events.PullRequestCreateEvent: 'created',
212 }.get(event.__class__, str(event.__class__))
212 }.get(event.__class__, str(event.__class__))
213
213
214 return ('Pull request <a href="{url}">#{number}</a> - {title} '
214 return ('Pull request <a href="{url}">#{number}</a> - {title} '
215 '{action} by <b>{user}</b>').format(
215 '{action} by <b>{user}</b>').format(
216 user=data['actor']['username'],
216 user=data['actor']['username'],
217 number=data['pullrequest']['pull_request_id'],
217 number=data['pullrequest']['pull_request_id'],
218 url=data['pullrequest']['url'],
218 url=data['pullrequest']['url'],
219 title=h.html_escape(data['pullrequest']['title']),
219 title=h.html_escape(data['pullrequest']['title']),
220 action=action
220 action=action
221 )
221 )
222
222
223 def format_repo_push_event(self, data):
223 def format_repo_push_event(self, data):
224 branches_commits = self.aggregate_branch_data(
224 branches_commits = self.aggregate_branch_data(
225 data['push']['branches'], data['push']['commits'])
225 data['push']['branches'], data['push']['commits'])
226
226
227 result = render_with_traceback(
227 result = render_with_traceback(
228 repo_push_template,
228 repo_push_template,
229 data=data,
229 data=data,
230 branches_commits=branches_commits,
230 branches_commits=branches_commits,
231 )
231 )
232 return result
232 return result
233
233
234 def format_repo_create_event(self, data):
234 def format_repo_create_event(self, data):
235 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
235 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
236 data['repo']['url'],
236 data['repo']['url'],
237 h.html_escape(data['repo']['repo_name']),
237 h.html_escape(data['repo']['repo_name']),
238 data['repo']['repo_type'],
238 data['repo']['repo_type'],
239 data['actor']['username'],
239 data['actor']['username'],
240 )
240 )
241
241
242
242
243 @async_task(ignore_result=True, base=RequestContextTask)
243 @async_task(ignore_result=True, base=RequestContextTask)
244 def post_text_to_hipchat(settings, text):
244 def post_text_to_hipchat(settings, text):
245 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
245 log.debug('sending %s to hipchat %s', text, settings['server_url'])
246 json_message = {
246 json_message = {
247 "message": text,
247 "message": text,
248 "color": settings.get('color', 'yellow'),
248 "color": settings.get('color', 'yellow'),
249 "notify": settings.get('notify', False),
249 "notify": settings.get('notify', False),
250 }
250 }
251
251
252 resp = requests.post(settings['server_url'], json=json_message, timeout=60)
252 resp = requests.post(settings['server_url'], json=json_message, timeout=60)
253 resp.raise_for_status() # raise exception on a failed request
253 resp.raise_for_status() # raise exception on a failed request
@@ -1,350 +1,349 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import re
22 import re
23 import time
23 import time
24 import textwrap
24 import textwrap
25 import logging
25 import logging
26
26
27 import deform
27 import deform
28 import requests
28 import requests
29 import colander
29 import colander
30 from mako.template import Template
30 from mako.template import Template
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.integrations.types.base import (
37 from rhodecode.integrations.types.base import (
38 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback)
38 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback)
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class SlackSettingsSchema(colander.Schema):
43 class SlackSettingsSchema(colander.Schema):
44 service = colander.SchemaNode(
44 service = colander.SchemaNode(
45 colander.String(),
45 colander.String(),
46 title=_('Slack service URL'),
46 title=_('Slack service URL'),
47 description=h.literal(_(
47 description=h.literal(_(
48 'This can be setup at the '
48 'This can be setup at the '
49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
50 'slack app manager</a>')),
50 'slack app manager</a>')),
51 default='',
51 default='',
52 preparer=strip_whitespace,
52 preparer=strip_whitespace,
53 validator=colander.url,
53 validator=colander.url,
54 widget=deform.widget.TextInputWidget(
54 widget=deform.widget.TextInputWidget(
55 placeholder='https://hooks.slack.com/services/...',
55 placeholder='https://hooks.slack.com/services/...',
56 ),
56 ),
57 )
57 )
58 username = colander.SchemaNode(
58 username = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 title=_('Username'),
60 title=_('Username'),
61 description=_('Username to show notifications coming from.'),
61 description=_('Username to show notifications coming from.'),
62 missing='Rhodecode',
62 missing='Rhodecode',
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 widget=deform.widget.TextInputWidget(
64 widget=deform.widget.TextInputWidget(
65 placeholder='Rhodecode'
65 placeholder='Rhodecode'
66 ),
66 ),
67 )
67 )
68 channel = colander.SchemaNode(
68 channel = colander.SchemaNode(
69 colander.String(),
69 colander.String(),
70 title=_('Channel'),
70 title=_('Channel'),
71 description=_('Channel to send notifications to.'),
71 description=_('Channel to send notifications to.'),
72 missing='',
72 missing='',
73 preparer=strip_whitespace,
73 preparer=strip_whitespace,
74 widget=deform.widget.TextInputWidget(
74 widget=deform.widget.TextInputWidget(
75 placeholder='#general'
75 placeholder='#general'
76 ),
76 ),
77 )
77 )
78 icon_emoji = colander.SchemaNode(
78 icon_emoji = colander.SchemaNode(
79 colander.String(),
79 colander.String(),
80 title=_('Emoji'),
80 title=_('Emoji'),
81 description=_('Emoji to use eg. :studio_microphone:'),
81 description=_('Emoji to use eg. :studio_microphone:'),
82 missing='',
82 missing='',
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 widget=deform.widget.TextInputWidget(
84 widget=deform.widget.TextInputWidget(
85 placeholder=':studio_microphone:'
85 placeholder=':studio_microphone:'
86 ),
86 ),
87 )
87 )
88
88
89
89
90 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
90 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
91 key = 'slack'
91 key = 'slack'
92 display_name = _('Slack')
92 display_name = _('Slack')
93 description = _('Send events such as repo pushes and pull requests to '
93 description = _('Send events such as repo pushes and pull requests to '
94 'your slack channel.')
94 'your slack channel.')
95
95
96 @classmethod
96 @classmethod
97 def icon(cls):
97 def icon(cls):
98 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
98 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
99
99
100 valid_events = [
100 valid_events = [
101 events.PullRequestCloseEvent,
101 events.PullRequestCloseEvent,
102 events.PullRequestMergeEvent,
102 events.PullRequestMergeEvent,
103 events.PullRequestUpdateEvent,
103 events.PullRequestUpdateEvent,
104 events.PullRequestCommentEvent,
104 events.PullRequestCommentEvent,
105 events.PullRequestReviewEvent,
105 events.PullRequestReviewEvent,
106 events.PullRequestCreateEvent,
106 events.PullRequestCreateEvent,
107 events.RepoPushEvent,
107 events.RepoPushEvent,
108 events.RepoCreateEvent,
108 events.RepoCreateEvent,
109 ]
109 ]
110
110
111 def send_event(self, event):
111 def send_event(self, event):
112 if event.__class__ not in self.valid_events:
112 if event.__class__ not in self.valid_events:
113 log.debug('event not valid: %r' % event)
113 log.debug('event not valid: %r', event)
114 return
114 return
115
115
116 if event.name not in self.settings['events']:
116 if event.name not in self.settings['events']:
117 log.debug('event ignored: %r' % event)
117 log.debug('event ignored: %r', event)
118 return
118 return
119
119
120 data = event.as_dict()
120 data = event.as_dict()
121
121
122 # defaults
122 # defaults
123 title = '*%s* caused a *%s* event' % (
123 title = '*%s* caused a *%s* event' % (
124 data['actor']['username'], event.name)
124 data['actor']['username'], event.name)
125 text = '*%s* caused a *%s* event' % (
125 text = '*%s* caused a *%s* event' % (
126 data['actor']['username'], event.name)
126 data['actor']['username'], event.name)
127 fields = None
127 fields = None
128 overrides = None
128 overrides = None
129
129
130 log.debug('handling slack event for %s' % event.name)
130 log.debug('handling slack event for %s', event.name)
131
131
132 if isinstance(event, events.PullRequestCommentEvent):
132 if isinstance(event, events.PullRequestCommentEvent):
133 (title, text, fields, overrides) \
133 (title, text, fields, overrides) \
134 = self.format_pull_request_comment_event(event, data)
134 = self.format_pull_request_comment_event(event, data)
135 elif isinstance(event, events.PullRequestReviewEvent):
135 elif isinstance(event, events.PullRequestReviewEvent):
136 title, text = self.format_pull_request_review_event(event, data)
136 title, text = self.format_pull_request_review_event(event, data)
137 elif isinstance(event, events.PullRequestEvent):
137 elif isinstance(event, events.PullRequestEvent):
138 title, text = self.format_pull_request_event(event, data)
138 title, text = self.format_pull_request_event(event, data)
139 elif isinstance(event, events.RepoPushEvent):
139 elif isinstance(event, events.RepoPushEvent):
140 title, text = self.format_repo_push_event(data)
140 title, text = self.format_repo_push_event(data)
141 elif isinstance(event, events.RepoCreateEvent):
141 elif isinstance(event, events.RepoCreateEvent):
142 title, text = self.format_repo_create_event(data)
142 title, text = self.format_repo_create_event(data)
143 else:
143 else:
144 log.error('unhandled event type: %r' % event)
144 log.error('unhandled event type: %r', event)
145
145
146 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
146 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
147
147
148 def settings_schema(self):
148 def settings_schema(self):
149 schema = SlackSettingsSchema()
149 schema = SlackSettingsSchema()
150 schema.add(colander.SchemaNode(
150 schema.add(colander.SchemaNode(
151 colander.Set(),
151 colander.Set(),
152 widget=deform.widget.CheckboxChoiceWidget(
152 widget=deform.widget.CheckboxChoiceWidget(
153 values=sorted(
153 values=sorted(
154 [(e.name, e.display_name) for e in self.valid_events]
154 [(e.name, e.display_name) for e in self.valid_events]
155 )
155 )
156 ),
156 ),
157 description="Events activated for this integration",
157 description="Events activated for this integration",
158 name='events'
158 name='events'
159 ))
159 ))
160
160
161 return schema
161 return schema
162
162
163 def format_pull_request_comment_event(self, event, data):
163 def format_pull_request_comment_event(self, event, data):
164 comment_text = data['comment']['text']
164 comment_text = data['comment']['text']
165 if len(comment_text) > 200:
165 if len(comment_text) > 200:
166 comment_text = '<{comment_url}|{comment_text}...>'.format(
166 comment_text = '<{comment_url}|{comment_text}...>'.format(
167 comment_text=comment_text[:200],
167 comment_text=comment_text[:200],
168 comment_url=data['comment']['url'],
168 comment_url=data['comment']['url'],
169 )
169 )
170
170
171 fields = None
171 fields = None
172 overrides = None
172 overrides = None
173 status_text = None
173 status_text = None
174
174
175 if data['comment']['status']:
175 if data['comment']['status']:
176 status_color = {
176 status_color = {
177 'approved': '#0ac878',
177 'approved': '#0ac878',
178 'rejected': '#e85e4d'}.get(data['comment']['status'])
178 'rejected': '#e85e4d'}.get(data['comment']['status'])
179
179
180 if status_color:
180 if status_color:
181 overrides = {"color": status_color}
181 overrides = {"color": status_color}
182
182
183 status_text = data['comment']['status']
183 status_text = data['comment']['status']
184
184
185 if data['comment']['file']:
185 if data['comment']['file']:
186 fields = [
186 fields = [
187 {
187 {
188 "title": "file",
188 "title": "file",
189 "value": data['comment']['file']
189 "value": data['comment']['file']
190 },
190 },
191 {
191 {
192 "title": "line",
192 "title": "line",
193 "value": data['comment']['line']
193 "value": data['comment']['line']
194 }
194 }
195 ]
195 ]
196
196
197 template = Template(textwrap.dedent(r'''
197 template = Template(textwrap.dedent(r'''
198 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
198 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
199 '''))
199 '''))
200 title = render_with_traceback(
200 title = render_with_traceback(
201 template, data=data, comment=event.comment)
201 template, data=data, comment=event.comment)
202
202
203 template = Template(textwrap.dedent(r'''
203 template = Template(textwrap.dedent(r'''
204 *pull request title*: ${pr_title}
204 *pull request title*: ${pr_title}
205 % if status_text:
205 % if status_text:
206 *submitted status*: `${status_text}`
206 *submitted status*: `${status_text}`
207 % endif
207 % endif
208 >>> ${comment_text}
208 >>> ${comment_text}
209 '''))
209 '''))
210 text = render_with_traceback(
210 text = render_with_traceback(
211 template,
211 template,
212 comment_text=comment_text,
212 comment_text=comment_text,
213 pr_title=data['pullrequest']['title'],
213 pr_title=data['pullrequest']['title'],
214 status_text=status_text)
214 status_text=status_text)
215
215
216 return title, text, fields, overrides
216 return title, text, fields, overrides
217
217
218 def format_pull_request_review_event(self, event, data):
218 def format_pull_request_review_event(self, event, data):
219 template = Template(textwrap.dedent(r'''
219 template = Template(textwrap.dedent(r'''
220 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
220 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
221 '''))
221 '''))
222 title = render_with_traceback(template, data=data)
222 title = render_with_traceback(template, data=data)
223
223
224 template = Template(textwrap.dedent(r'''
224 template = Template(textwrap.dedent(r'''
225 *pull request title*: ${pr_title}
225 *pull request title*: ${pr_title}
226 '''))
226 '''))
227 text = render_with_traceback(
227 text = render_with_traceback(
228 template,
228 template,
229 pr_title=data['pullrequest']['title'])
229 pr_title=data['pullrequest']['title'])
230
230
231 return title, text
231 return title, text
232
232
233 def format_pull_request_event(self, event, data):
233 def format_pull_request_event(self, event, data):
234 action = {
234 action = {
235 events.PullRequestCloseEvent: 'closed',
235 events.PullRequestCloseEvent: 'closed',
236 events.PullRequestMergeEvent: 'merged',
236 events.PullRequestMergeEvent: 'merged',
237 events.PullRequestUpdateEvent: 'updated',
237 events.PullRequestUpdateEvent: 'updated',
238 events.PullRequestCreateEvent: 'created',
238 events.PullRequestCreateEvent: 'created',
239 }.get(event.__class__, str(event.__class__))
239 }.get(event.__class__, str(event.__class__))
240
240
241 template = Template(textwrap.dedent(r'''
241 template = Template(textwrap.dedent(r'''
242 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
242 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
243 '''))
243 '''))
244 title = render_with_traceback(template, data=data, action=action)
244 title = render_with_traceback(template, data=data, action=action)
245
245
246 template = Template(textwrap.dedent(r'''
246 template = Template(textwrap.dedent(r'''
247 *pull request title*: ${pr_title}
247 *pull request title*: ${pr_title}
248 %if data['pullrequest']['commits']:
248 %if data['pullrequest']['commits']:
249 *commits*: ${len(data['pullrequest']['commits'])}
249 *commits*: ${len(data['pullrequest']['commits'])}
250 %endif
250 %endif
251 '''))
251 '''))
252 text = render_with_traceback(
252 text = render_with_traceback(
253 template,
253 template,
254 pr_title=data['pullrequest']['title'],
254 pr_title=data['pullrequest']['title'],
255 data=data)
255 data=data)
256
256
257 return title, text
257 return title, text
258
258
259 def format_repo_push_event(self, data):
259 def format_repo_push_event(self, data):
260
260
261 branches_commits = self.aggregate_branch_data(
261 branches_commits = self.aggregate_branch_data(
262 data['push']['branches'], data['push']['commits'])
262 data['push']['branches'], data['push']['commits'])
263
263
264 template = Template(r'''
264 template = Template(r'''
265 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
265 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
266 ''')
266 ''')
267 title = render_with_traceback(template, data=data)
267 title = render_with_traceback(template, data=data)
268
268
269 repo_push_template = Template(textwrap.dedent(r'''
269 repo_push_template = Template(textwrap.dedent(r'''
270 <%
270 <%
271 def branch_text(branch):
271 def branch_text(branch):
272 if branch:
272 if branch:
273 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
273 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
274 else:
274 else:
275 ## case for SVN no branch push...
275 ## case for SVN no branch push...
276 return 'to trunk'
276 return 'to trunk'
277 %> \
277 %> \
278 % for branch, branch_commits in branches_commits.items():
278 % for branch, branch_commits in branches_commits.items():
279 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
279 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
280 % for commit in branch_commits['commits']:
280 % for commit in branch_commits['commits']:
281 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
281 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
282 % endfor
282 % endfor
283 % endfor
283 % endfor
284 '''))
284 '''))
285
285
286 text = render_with_traceback(
286 text = render_with_traceback(
287 repo_push_template,
287 repo_push_template,
288 data=data,
288 data=data,
289 branches_commits=branches_commits,
289 branches_commits=branches_commits,
290 html_to_slack_links=html_to_slack_links,
290 html_to_slack_links=html_to_slack_links,
291 )
291 )
292
292
293 return title, text
293 return title, text
294
294
295 def format_repo_create_event(self, data):
295 def format_repo_create_event(self, data):
296 template = Template(r'''
296 template = Template(r'''
297 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
297 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
298 ''')
298 ''')
299 title = render_with_traceback(template, data=data)
299 title = render_with_traceback(template, data=data)
300
300
301 template = Template(textwrap.dedent(r'''
301 template = Template(textwrap.dedent(r'''
302 repo_url: ${data['repo']['url']}
302 repo_url: ${data['repo']['url']}
303 repo_type: ${data['repo']['repo_type']}
303 repo_type: ${data['repo']['repo_type']}
304 '''))
304 '''))
305 text = render_with_traceback(template, data=data)
305 text = render_with_traceback(template, data=data)
306
306
307 return title, text
307 return title, text
308
308
309
309
310 def html_to_slack_links(message):
310 def html_to_slack_links(message):
311 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
311 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
312 r'<\1|\2>', message)
312 r'<\1|\2>', message)
313
313
314
314
315 @async_task(ignore_result=True, base=RequestContextTask)
315 @async_task(ignore_result=True, base=RequestContextTask)
316 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
316 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
317 log.debug('sending %s (%s) to slack %s' % (
317 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
318 title, text, settings['service']))
319
318
320 fields = fields or []
319 fields = fields or []
321 overrides = overrides or {}
320 overrides = overrides or {}
322
321
323 message_data = {
322 message_data = {
324 "fallback": text,
323 "fallback": text,
325 "color": "#427cc9",
324 "color": "#427cc9",
326 "pretext": title,
325 "pretext": title,
327 #"author_name": "Bobby Tables",
326 #"author_name": "Bobby Tables",
328 #"author_link": "http://flickr.com/bobby/",
327 #"author_link": "http://flickr.com/bobby/",
329 #"author_icon": "http://flickr.com/icons/bobby.jpg",
328 #"author_icon": "http://flickr.com/icons/bobby.jpg",
330 #"title": "Slack API Documentation",
329 #"title": "Slack API Documentation",
331 #"title_link": "https://api.slack.com/",
330 #"title_link": "https://api.slack.com/",
332 "text": text,
331 "text": text,
333 "fields": fields,
332 "fields": fields,
334 #"image_url": "http://my-website.com/path/to/image.jpg",
333 #"image_url": "http://my-website.com/path/to/image.jpg",
335 #"thumb_url": "http://example.com/path/to/thumb.png",
334 #"thumb_url": "http://example.com/path/to/thumb.png",
336 "footer": "RhodeCode",
335 "footer": "RhodeCode",
337 #"footer_icon": "",
336 #"footer_icon": "",
338 "ts": time.time(),
337 "ts": time.time(),
339 "mrkdwn_in": ["pretext", "text"]
338 "mrkdwn_in": ["pretext", "text"]
340 }
339 }
341 message_data.update(overrides)
340 message_data.update(overrides)
342 json_message = {
341 json_message = {
343 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
342 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
344 "channel": settings.get('channel', ''),
343 "channel": settings.get('channel', ''),
345 "username": settings.get('username', 'Rhodecode'),
344 "username": settings.get('username', 'Rhodecode'),
346 "attachments": [message_data]
345 "attachments": [message_data]
347 }
346 }
348
347
349 resp = requests.post(settings['service'], json=json_message, timeout=60)
348 resp = requests.post(settings['service'], json=json_message, timeout=60)
350 resp.raise_for_status() # raise exception on a failed request
349 resp.raise_for_status() # raise exception on a failed request
@@ -1,274 +1,274 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22
22
23 import deform
23 import deform
24 import deform.widget
24 import deform.widget
25 import logging
25 import logging
26 import requests
26 import requests
27 import requests.adapters
27 import requests.adapters
28 import colander
28 import colander
29 from requests.packages.urllib3.util.retry import Retry
29 from requests.packages.urllib3.util.retry import Retry
30
30
31 import rhodecode
31 import rhodecode
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.integrations.types.base import (
34 from rhodecode.integrations.types.base import (
35 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
35 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
36 WebhookDataHandler, WEBHOOK_URL_VARS)
36 WebhookDataHandler, WEBHOOK_URL_VARS)
37 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
37 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
38 from rhodecode.model.validation_schema import widgets
38 from rhodecode.model.validation_schema import widgets
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 # updating this required to update the `common_vars` passed in url calling func
43 # updating this required to update the `common_vars` passed in url calling func
44
44
45 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
45 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
46
46
47
47
48 class WebhookSettingsSchema(colander.Schema):
48 class WebhookSettingsSchema(colander.Schema):
49 url = colander.SchemaNode(
49 url = colander.SchemaNode(
50 colander.String(),
50 colander.String(),
51 title=_('Webhook URL'),
51 title=_('Webhook URL'),
52 description=
52 description=
53 _('URL to which Webhook should submit data. If used some of the '
53 _('URL to which Webhook should submit data. If used some of the '
54 'variables would trigger multiple calls, like ${branch} or '
54 'variables would trigger multiple calls, like ${branch} or '
55 '${commit_id}. Webhook will be called as many times as unique '
55 '${commit_id}. Webhook will be called as many times as unique '
56 'objects in data in such cases.'),
56 'objects in data in such cases.'),
57 missing=colander.required,
57 missing=colander.required,
58 required=True,
58 required=True,
59 validator=colander.url,
59 validator=colander.url,
60 widget=widgets.CodeMirrorWidget(
60 widget=widgets.CodeMirrorWidget(
61 help_block_collapsable_name='Show url variables',
61 help_block_collapsable_name='Show url variables',
62 help_block_collapsable=(
62 help_block_collapsable=(
63 'E.g http://my-serv/trigger_job/${{event_name}}'
63 'E.g http://my-serv/trigger_job/${{event_name}}'
64 '?PR_ID=${{pull_request_id}}'
64 '?PR_ID=${{pull_request_id}}'
65 '\nFull list of vars:\n{}'.format(URL_VARS)),
65 '\nFull list of vars:\n{}'.format(URL_VARS)),
66 codemirror_mode='text',
66 codemirror_mode='text',
67 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
67 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
68 )
68 )
69 secret_token = colander.SchemaNode(
69 secret_token = colander.SchemaNode(
70 colander.String(),
70 colander.String(),
71 title=_('Secret Token'),
71 title=_('Secret Token'),
72 description=_('Optional string used to validate received payloads. '
72 description=_('Optional string used to validate received payloads. '
73 'It will be sent together with event data in JSON'),
73 'It will be sent together with event data in JSON'),
74 default='',
74 default='',
75 missing='',
75 missing='',
76 widget=deform.widget.TextInputWidget(
76 widget=deform.widget.TextInputWidget(
77 placeholder='e.g. secret_token'
77 placeholder='e.g. secret_token'
78 ),
78 ),
79 )
79 )
80 username = colander.SchemaNode(
80 username = colander.SchemaNode(
81 colander.String(),
81 colander.String(),
82 title=_('Username'),
82 title=_('Username'),
83 description=_('Optional username to authenticate the call.'),
83 description=_('Optional username to authenticate the call.'),
84 default='',
84 default='',
85 missing='',
85 missing='',
86 widget=deform.widget.TextInputWidget(
86 widget=deform.widget.TextInputWidget(
87 placeholder='e.g. admin'
87 placeholder='e.g. admin'
88 ),
88 ),
89 )
89 )
90 password = colander.SchemaNode(
90 password = colander.SchemaNode(
91 colander.String(),
91 colander.String(),
92 title=_('Password'),
92 title=_('Password'),
93 description=_('Optional password to authenticate the call.'),
93 description=_('Optional password to authenticate the call.'),
94 default='',
94 default='',
95 missing='',
95 missing='',
96 widget=deform.widget.PasswordWidget(
96 widget=deform.widget.PasswordWidget(
97 placeholder='e.g. secret.',
97 placeholder='e.g. secret.',
98 redisplay=True,
98 redisplay=True,
99 ),
99 ),
100 )
100 )
101 custom_header_key = colander.SchemaNode(
101 custom_header_key = colander.SchemaNode(
102 colander.String(),
102 colander.String(),
103 title=_('Custom Header Key'),
103 title=_('Custom Header Key'),
104 description=_('Custom Header name to be set when calling endpoint.'),
104 description=_('Custom Header name to be set when calling endpoint.'),
105 default='',
105 default='',
106 missing='',
106 missing='',
107 widget=deform.widget.TextInputWidget(
107 widget=deform.widget.TextInputWidget(
108 placeholder='e.g: Authorization'
108 placeholder='e.g: Authorization'
109 ),
109 ),
110 )
110 )
111 custom_header_val = colander.SchemaNode(
111 custom_header_val = colander.SchemaNode(
112 colander.String(),
112 colander.String(),
113 title=_('Custom Header Value'),
113 title=_('Custom Header Value'),
114 description=_('Custom Header value to be set when calling endpoint.'),
114 description=_('Custom Header value to be set when calling endpoint.'),
115 default='',
115 default='',
116 missing='',
116 missing='',
117 widget=deform.widget.TextInputWidget(
117 widget=deform.widget.TextInputWidget(
118 placeholder='e.g. Basic XxXxXx'
118 placeholder='e.g. Basic XxXxXx'
119 ),
119 ),
120 )
120 )
121 method_type = colander.SchemaNode(
121 method_type = colander.SchemaNode(
122 colander.String(),
122 colander.String(),
123 title=_('Call Method'),
123 title=_('Call Method'),
124 description=_('Select if the Webhook call should be made '
124 description=_('Select if the Webhook call should be made '
125 'with POST or GET.'),
125 'with POST or GET.'),
126 default='post',
126 default='post',
127 missing='',
127 missing='',
128 widget=deform.widget.RadioChoiceWidget(
128 widget=deform.widget.RadioChoiceWidget(
129 values=[('get', 'GET'), ('post', 'POST')],
129 values=[('get', 'GET'), ('post', 'POST')],
130 inline=True
130 inline=True
131 ),
131 ),
132 )
132 )
133
133
134
134
135 class WebhookIntegrationType(IntegrationTypeBase):
135 class WebhookIntegrationType(IntegrationTypeBase):
136 key = 'webhook'
136 key = 'webhook'
137 display_name = _('Webhook')
137 display_name = _('Webhook')
138 description = _('send JSON data to a url endpoint')
138 description = _('send JSON data to a url endpoint')
139
139
140 @classmethod
140 @classmethod
141 def icon(cls):
141 def icon(cls):
142 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
142 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
143
143
144 valid_events = [
144 valid_events = [
145 events.PullRequestCloseEvent,
145 events.PullRequestCloseEvent,
146 events.PullRequestMergeEvent,
146 events.PullRequestMergeEvent,
147 events.PullRequestUpdateEvent,
147 events.PullRequestUpdateEvent,
148 events.PullRequestCommentEvent,
148 events.PullRequestCommentEvent,
149 events.PullRequestReviewEvent,
149 events.PullRequestReviewEvent,
150 events.PullRequestCreateEvent,
150 events.PullRequestCreateEvent,
151 events.RepoPushEvent,
151 events.RepoPushEvent,
152 events.RepoCreateEvent,
152 events.RepoCreateEvent,
153 ]
153 ]
154
154
155 def settings_schema(self):
155 def settings_schema(self):
156 schema = WebhookSettingsSchema()
156 schema = WebhookSettingsSchema()
157 schema.add(colander.SchemaNode(
157 schema.add(colander.SchemaNode(
158 colander.Set(),
158 colander.Set(),
159 widget=deform.widget.CheckboxChoiceWidget(
159 widget=deform.widget.CheckboxChoiceWidget(
160 values=sorted(
160 values=sorted(
161 [(e.name, e.display_name) for e in self.valid_events]
161 [(e.name, e.display_name) for e in self.valid_events]
162 )
162 )
163 ),
163 ),
164 description="Events activated for this integration",
164 description="Events activated for this integration",
165 name='events'
165 name='events'
166 ))
166 ))
167 return schema
167 return schema
168
168
169 def send_event(self, event):
169 def send_event(self, event):
170 log.debug(
170 log.debug(
171 'handling event %s with Webhook integration %s', event.name, self)
171 'handling event %s with Webhook integration %s', event.name, self)
172
172
173 if event.__class__ not in self.valid_events:
173 if event.__class__ not in self.valid_events:
174 log.debug('event not valid: %r' % event)
174 log.debug('event not valid: %r', event)
175 return
175 return
176
176
177 if event.name not in self.settings['events']:
177 if event.name not in self.settings['events']:
178 log.debug('event ignored: %r' % event)
178 log.debug('event ignored: %r', event)
179 return
179 return
180
180
181 data = event.as_dict()
181 data = event.as_dict()
182 template_url = self.settings['url']
182 template_url = self.settings['url']
183
183
184 headers = {}
184 headers = {}
185 head_key = self.settings.get('custom_header_key')
185 head_key = self.settings.get('custom_header_key')
186 head_val = self.settings.get('custom_header_val')
186 head_val = self.settings.get('custom_header_val')
187 if head_key and head_val:
187 if head_key and head_val:
188 headers = {head_key: head_val}
188 headers = {head_key: head_val}
189
189
190 handler = WebhookDataHandler(template_url, headers)
190 handler = WebhookDataHandler(template_url, headers)
191
191
192 url_calls = handler(event, data)
192 url_calls = handler(event, data)
193 log.debug('webhook: calling following urls: %s',
193 log.debug('webhook: calling following urls: %s',
194 [x[0] for x in url_calls])
194 [x[0] for x in url_calls])
195
195
196 run_task(post_to_webhook, url_calls, self.settings)
196 run_task(post_to_webhook, url_calls, self.settings)
197
197
198
198
199 @async_task(ignore_result=True, base=RequestContextTask)
199 @async_task(ignore_result=True, base=RequestContextTask)
200 def post_to_webhook(url_calls, settings):
200 def post_to_webhook(url_calls, settings):
201 """
201 """
202 Example data::
202 Example data::
203
203
204 {'actor': {'user_id': 2, 'username': u'admin'},
204 {'actor': {'user_id': 2, 'username': u'admin'},
205 'actor_ip': u'192.168.157.1',
205 'actor_ip': u'192.168.157.1',
206 'name': 'repo-push',
206 'name': 'repo-push',
207 'push': {'branches': [{'name': u'default',
207 'push': {'branches': [{'name': u'default',
208 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
208 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
209 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
209 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
210 'branch': u'default',
210 'branch': u'default',
211 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
211 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
212 'issues': [],
212 'issues': [],
213 'mentions': [],
213 'mentions': [],
214 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
214 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
215 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
215 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
216 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
216 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
217 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
217 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
218 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
218 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
219 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
219 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
220 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
220 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
221 'reviewers': [],
221 'reviewers': [],
222 'revision': 9L,
222 'revision': 9L,
223 'short_id': 'a815cc738b96',
223 'short_id': 'a815cc738b96',
224 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
224 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
225 'issues': {}},
225 'issues': {}},
226 'repo': {'extra_fields': '',
226 'repo': {'extra_fields': '',
227 'permalink_url': u'http://rc.local:8080/_7',
227 'permalink_url': u'http://rc.local:8080/_7',
228 'repo_id': 7,
228 'repo_id': 7,
229 'repo_name': u'hg-repo',
229 'repo_name': u'hg-repo',
230 'repo_type': u'hg',
230 'repo_type': u'hg',
231 'url': u'http://rc.local:8080/hg-repo'},
231 'url': u'http://rc.local:8080/hg-repo'},
232 'server_url': u'http://rc.local:8080',
232 'server_url': u'http://rc.local:8080',
233 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
233 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
234
234
235 """
235 """
236 max_retries = 3
236 max_retries = 3
237 retries = Retry(
237 retries = Retry(
238 total=max_retries,
238 total=max_retries,
239 backoff_factor=0.15,
239 backoff_factor=0.15,
240 status_forcelist=[500, 502, 503, 504])
240 status_forcelist=[500, 502, 503, 504])
241 call_headers = {
241 call_headers = {
242 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
242 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
243 rhodecode.__version__)
243 rhodecode.__version__)
244 } # updated below with custom ones, allows override
244 } # updated below with custom ones, allows override
245
245
246 auth = get_auth(settings)
246 auth = get_auth(settings)
247 token = get_web_token(settings)
247 token = get_web_token(settings)
248
248
249 for url, headers, data in url_calls:
249 for url, headers, data in url_calls:
250 req_session = requests.Session()
250 req_session = requests.Session()
251 req_session.mount( # retry max N times
251 req_session.mount( # retry max N times
252 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
252 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
253
253
254 method = settings.get('method_type') or 'post'
254 method = settings.get('method_type') or 'post'
255 call_method = getattr(req_session, method)
255 call_method = getattr(req_session, method)
256
256
257 headers = headers or {}
257 headers = headers or {}
258 call_headers.update(headers)
258 call_headers.update(headers)
259
259
260 log.debug('calling Webhook with method: %s, and auth:%s',
260 log.debug('calling Webhook with method: %s, and auth:%s',
261 call_method, auth)
261 call_method, auth)
262 if settings.get('log_data'):
262 if settings.get('log_data'):
263 log.debug('calling webhook with data: %s', data)
263 log.debug('calling webhook with data: %s', data)
264 resp = call_method(url, json={
264 resp = call_method(url, json={
265 'token': token,
265 'token': token,
266 'event': data
266 'event': data
267 }, headers=call_headers, auth=auth, timeout=60)
267 }, headers=call_headers, auth=auth, timeout=60)
268 log.debug('Got Webhook response: %s', resp)
268 log.debug('Got Webhook response: %s', resp)
269
269
270 try:
270 try:
271 resp.raise_for_status() # raise exception on a failed request
271 resp.raise_for_status() # raise exception on a failed request
272 except Exception:
272 except Exception:
273 log.error(resp.text)
273 log.error(resp.text)
274 raise
274 raise
@@ -1,2342 +1,2338 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 authentication and permission libraries
22 authentication and permission libraries
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import inspect
27 import inspect
28 import collections
28 import collections
29 import fnmatch
29 import fnmatch
30 import hashlib
30 import hashlib
31 import itertools
31 import itertools
32 import logging
32 import logging
33 import random
33 import random
34 import traceback
34 import traceback
35 from functools import wraps
35 from functools import wraps
36
36
37 import ipaddress
37 import ipaddress
38
38
39 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
39 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
40 from sqlalchemy.orm.exc import ObjectDeletedError
40 from sqlalchemy.orm.exc import ObjectDeletedError
41 from sqlalchemy.orm import joinedload
41 from sqlalchemy.orm import joinedload
42 from zope.cachedescriptors.property import Lazy as LazyProperty
42 from zope.cachedescriptors.property import Lazy as LazyProperty
43
43
44 import rhodecode
44 import rhodecode
45 from rhodecode.model import meta
45 from rhodecode.model import meta
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
49 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
50 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
50 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
51 from rhodecode.lib import rc_cache
51 from rhodecode.lib import rc_cache
52 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
52 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
53 from rhodecode.lib.utils import (
53 from rhodecode.lib.utils import (
54 get_repo_slug, get_repo_group_slug, get_user_group_slug)
54 get_repo_slug, get_repo_group_slug, get_user_group_slug)
55 from rhodecode.lib.caching_query import FromCache
55 from rhodecode.lib.caching_query import FromCache
56
56
57
57
58 if rhodecode.is_unix:
58 if rhodecode.is_unix:
59 import bcrypt
59 import bcrypt
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63 csrf_token_key = "csrf_token"
63 csrf_token_key = "csrf_token"
64
64
65
65
66 class PasswordGenerator(object):
66 class PasswordGenerator(object):
67 """
67 """
68 This is a simple class for generating password from different sets of
68 This is a simple class for generating password from different sets of
69 characters
69 characters
70 usage::
70 usage::
71
71
72 passwd_gen = PasswordGenerator()
72 passwd_gen = PasswordGenerator()
73 #print 8-letter password containing only big and small letters
73 #print 8-letter password containing only big and small letters
74 of alphabet
74 of alphabet
75 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
76 """
76 """
77 ALPHABETS_NUM = r'''1234567890'''
77 ALPHABETS_NUM = r'''1234567890'''
78 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
79 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
80 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
81 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
82 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 + ALPHABETS_NUM + ALPHABETS_SPECIAL
83 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
84 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
85 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
86 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
87
87
88 def __init__(self, passwd=''):
88 def __init__(self, passwd=''):
89 self.passwd = passwd
89 self.passwd = passwd
90
90
91 def gen_password(self, length, type_=None):
91 def gen_password(self, length, type_=None):
92 if type_ is None:
92 if type_ is None:
93 type_ = self.ALPHABETS_FULL
93 type_ = self.ALPHABETS_FULL
94 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
94 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
95 return self.passwd
95 return self.passwd
96
96
97
97
98 class _RhodeCodeCryptoBase(object):
98 class _RhodeCodeCryptoBase(object):
99 ENC_PREF = None
99 ENC_PREF = None
100
100
101 def hash_create(self, str_):
101 def hash_create(self, str_):
102 """
102 """
103 hash the string using
103 hash the string using
104
104
105 :param str_: password to hash
105 :param str_: password to hash
106 """
106 """
107 raise NotImplementedError
107 raise NotImplementedError
108
108
109 def hash_check_with_upgrade(self, password, hashed):
109 def hash_check_with_upgrade(self, password, hashed):
110 """
110 """
111 Returns tuple in which first element is boolean that states that
111 Returns tuple in which first element is boolean that states that
112 given password matches it's hashed version, and the second is new hash
112 given password matches it's hashed version, and the second is new hash
113 of the password, in case this password should be migrated to new
113 of the password, in case this password should be migrated to new
114 cipher.
114 cipher.
115 """
115 """
116 checked_hash = self.hash_check(password, hashed)
116 checked_hash = self.hash_check(password, hashed)
117 return checked_hash, None
117 return checked_hash, None
118
118
119 def hash_check(self, password, hashed):
119 def hash_check(self, password, hashed):
120 """
120 """
121 Checks matching password with it's hashed value.
121 Checks matching password with it's hashed value.
122
122
123 :param password: password
123 :param password: password
124 :param hashed: password in hashed form
124 :param hashed: password in hashed form
125 """
125 """
126 raise NotImplementedError
126 raise NotImplementedError
127
127
128 def _assert_bytes(self, value):
128 def _assert_bytes(self, value):
129 """
129 """
130 Passing in an `unicode` object can lead to hard to detect issues
130 Passing in an `unicode` object can lead to hard to detect issues
131 if passwords contain non-ascii characters. Doing a type check
131 if passwords contain non-ascii characters. Doing a type check
132 during runtime, so that such mistakes are detected early on.
132 during runtime, so that such mistakes are detected early on.
133 """
133 """
134 if not isinstance(value, str):
134 if not isinstance(value, str):
135 raise TypeError(
135 raise TypeError(
136 "Bytestring required as input, got %r." % (value, ))
136 "Bytestring required as input, got %r." % (value, ))
137
137
138
138
139 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
139 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
140 ENC_PREF = ('$2a$10', '$2b$10')
140 ENC_PREF = ('$2a$10', '$2b$10')
141
141
142 def hash_create(self, str_):
142 def hash_create(self, str_):
143 self._assert_bytes(str_)
143 self._assert_bytes(str_)
144 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
144 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
145
145
146 def hash_check_with_upgrade(self, password, hashed):
146 def hash_check_with_upgrade(self, password, hashed):
147 """
147 """
148 Returns tuple in which first element is boolean that states that
148 Returns tuple in which first element is boolean that states that
149 given password matches it's hashed version, and the second is new hash
149 given password matches it's hashed version, and the second is new hash
150 of the password, in case this password should be migrated to new
150 of the password, in case this password should be migrated to new
151 cipher.
151 cipher.
152
152
153 This implements special upgrade logic which works like that:
153 This implements special upgrade logic which works like that:
154 - check if the given password == bcrypted hash, if yes then we
154 - check if the given password == bcrypted hash, if yes then we
155 properly used password and it was already in bcrypt. Proceed
155 properly used password and it was already in bcrypt. Proceed
156 without any changes
156 without any changes
157 - if bcrypt hash check is not working try with sha256. If hash compare
157 - if bcrypt hash check is not working try with sha256. If hash compare
158 is ok, it means we using correct but old hashed password. indicate
158 is ok, it means we using correct but old hashed password. indicate
159 hash change and proceed
159 hash change and proceed
160 """
160 """
161
161
162 new_hash = None
162 new_hash = None
163
163
164 # regular pw check
164 # regular pw check
165 password_match_bcrypt = self.hash_check(password, hashed)
165 password_match_bcrypt = self.hash_check(password, hashed)
166
166
167 # now we want to know if the password was maybe from sha256
167 # now we want to know if the password was maybe from sha256
168 # basically calling _RhodeCodeCryptoSha256().hash_check()
168 # basically calling _RhodeCodeCryptoSha256().hash_check()
169 if not password_match_bcrypt:
169 if not password_match_bcrypt:
170 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
170 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
171 new_hash = self.hash_create(password) # make new bcrypt hash
171 new_hash = self.hash_create(password) # make new bcrypt hash
172 password_match_bcrypt = True
172 password_match_bcrypt = True
173
173
174 return password_match_bcrypt, new_hash
174 return password_match_bcrypt, new_hash
175
175
176 def hash_check(self, password, hashed):
176 def hash_check(self, password, hashed):
177 """
177 """
178 Checks matching password with it's hashed value.
178 Checks matching password with it's hashed value.
179
179
180 :param password: password
180 :param password: password
181 :param hashed: password in hashed form
181 :param hashed: password in hashed form
182 """
182 """
183 self._assert_bytes(password)
183 self._assert_bytes(password)
184 try:
184 try:
185 return bcrypt.hashpw(password, hashed) == hashed
185 return bcrypt.hashpw(password, hashed) == hashed
186 except ValueError as e:
186 except ValueError as e:
187 # we're having a invalid salt here probably, we should not crash
187 # we're having a invalid salt here probably, we should not crash
188 # just return with False as it would be a wrong password.
188 # just return with False as it would be a wrong password.
189 log.debug('Failed to check password hash using bcrypt %s',
189 log.debug('Failed to check password hash using bcrypt %s',
190 safe_str(e))
190 safe_str(e))
191
191
192 return False
192 return False
193
193
194
194
195 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
195 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
196 ENC_PREF = '_'
196 ENC_PREF = '_'
197
197
198 def hash_create(self, str_):
198 def hash_create(self, str_):
199 self._assert_bytes(str_)
199 self._assert_bytes(str_)
200 return hashlib.sha256(str_).hexdigest()
200 return hashlib.sha256(str_).hexdigest()
201
201
202 def hash_check(self, password, hashed):
202 def hash_check(self, password, hashed):
203 """
203 """
204 Checks matching password with it's hashed value.
204 Checks matching password with it's hashed value.
205
205
206 :param password: password
206 :param password: password
207 :param hashed: password in hashed form
207 :param hashed: password in hashed form
208 """
208 """
209 self._assert_bytes(password)
209 self._assert_bytes(password)
210 return hashlib.sha256(password).hexdigest() == hashed
210 return hashlib.sha256(password).hexdigest() == hashed
211
211
212
212
213 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
213 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
214 ENC_PREF = '_'
214 ENC_PREF = '_'
215
215
216 def hash_create(self, str_):
216 def hash_create(self, str_):
217 self._assert_bytes(str_)
217 self._assert_bytes(str_)
218 return sha1(str_)
218 return sha1(str_)
219
219
220 def hash_check(self, password, hashed):
220 def hash_check(self, password, hashed):
221 """
221 """
222 Checks matching password with it's hashed value.
222 Checks matching password with it's hashed value.
223
223
224 :param password: password
224 :param password: password
225 :param hashed: password in hashed form
225 :param hashed: password in hashed form
226 """
226 """
227 self._assert_bytes(password)
227 self._assert_bytes(password)
228 return sha1(password) == hashed
228 return sha1(password) == hashed
229
229
230
230
231 def crypto_backend():
231 def crypto_backend():
232 """
232 """
233 Return the matching crypto backend.
233 Return the matching crypto backend.
234
234
235 Selection is based on if we run tests or not, we pick sha1-test backend to run
235 Selection is based on if we run tests or not, we pick sha1-test backend to run
236 tests faster since BCRYPT is expensive to calculate
236 tests faster since BCRYPT is expensive to calculate
237 """
237 """
238 if rhodecode.is_test:
238 if rhodecode.is_test:
239 RhodeCodeCrypto = _RhodeCodeCryptoTest()
239 RhodeCodeCrypto = _RhodeCodeCryptoTest()
240 else:
240 else:
241 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
242
242
243 return RhodeCodeCrypto
243 return RhodeCodeCrypto
244
244
245
245
246 def get_crypt_password(password):
246 def get_crypt_password(password):
247 """
247 """
248 Create the hash of `password` with the active crypto backend.
248 Create the hash of `password` with the active crypto backend.
249
249
250 :param password: The cleartext password.
250 :param password: The cleartext password.
251 :type password: unicode
251 :type password: unicode
252 """
252 """
253 password = safe_str(password)
253 password = safe_str(password)
254 return crypto_backend().hash_create(password)
254 return crypto_backend().hash_create(password)
255
255
256
256
257 def check_password(password, hashed):
257 def check_password(password, hashed):
258 """
258 """
259 Check if the value in `password` matches the hash in `hashed`.
259 Check if the value in `password` matches the hash in `hashed`.
260
260
261 :param password: The cleartext password.
261 :param password: The cleartext password.
262 :type password: unicode
262 :type password: unicode
263
263
264 :param hashed: The expected hashed version of the password.
264 :param hashed: The expected hashed version of the password.
265 :type hashed: The hash has to be passed in in text representation.
265 :type hashed: The hash has to be passed in in text representation.
266 """
266 """
267 password = safe_str(password)
267 password = safe_str(password)
268 return crypto_backend().hash_check(password, hashed)
268 return crypto_backend().hash_check(password, hashed)
269
269
270
270
271 def generate_auth_token(data, salt=None):
271 def generate_auth_token(data, salt=None):
272 """
272 """
273 Generates API KEY from given string
273 Generates API KEY from given string
274 """
274 """
275
275
276 if salt is None:
276 if salt is None:
277 salt = os.urandom(16)
277 salt = os.urandom(16)
278 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 return hashlib.sha1(safe_str(data) + salt).hexdigest()
279
279
280
280
281 def get_came_from(request):
281 def get_came_from(request):
282 """
282 """
283 get query_string+path from request sanitized after removing auth_token
283 get query_string+path from request sanitized after removing auth_token
284 """
284 """
285 _req = request
285 _req = request
286
286
287 path = _req.path
287 path = _req.path
288 if 'auth_token' in _req.GET:
288 if 'auth_token' in _req.GET:
289 # sanitize the request and remove auth_token for redirection
289 # sanitize the request and remove auth_token for redirection
290 _req.GET.pop('auth_token')
290 _req.GET.pop('auth_token')
291 qs = _req.query_string
291 qs = _req.query_string
292 if qs:
292 if qs:
293 path += '?' + qs
293 path += '?' + qs
294
294
295 return path
295 return path
296
296
297
297
298 class CookieStoreWrapper(object):
298 class CookieStoreWrapper(object):
299
299
300 def __init__(self, cookie_store):
300 def __init__(self, cookie_store):
301 self.cookie_store = cookie_store
301 self.cookie_store = cookie_store
302
302
303 def __repr__(self):
303 def __repr__(self):
304 return 'CookieStore<%s>' % (self.cookie_store)
304 return 'CookieStore<%s>' % (self.cookie_store)
305
305
306 def get(self, key, other=None):
306 def get(self, key, other=None):
307 if isinstance(self.cookie_store, dict):
307 if isinstance(self.cookie_store, dict):
308 return self.cookie_store.get(key, other)
308 return self.cookie_store.get(key, other)
309 elif isinstance(self.cookie_store, AuthUser):
309 elif isinstance(self.cookie_store, AuthUser):
310 return self.cookie_store.__dict__.get(key, other)
310 return self.cookie_store.__dict__.get(key, other)
311
311
312
312
313 def _cached_perms_data(user_id, scope, user_is_admin,
313 def _cached_perms_data(user_id, scope, user_is_admin,
314 user_inherit_default_permissions, explicit, algo,
314 user_inherit_default_permissions, explicit, algo,
315 calculate_super_admin):
315 calculate_super_admin):
316
316
317 permissions = PermissionCalculator(
317 permissions = PermissionCalculator(
318 user_id, scope, user_is_admin, user_inherit_default_permissions,
318 user_id, scope, user_is_admin, user_inherit_default_permissions,
319 explicit, algo, calculate_super_admin)
319 explicit, algo, calculate_super_admin)
320 return permissions.calculate()
320 return permissions.calculate()
321
321
322
322
323 class PermOrigin(object):
323 class PermOrigin(object):
324 SUPER_ADMIN = 'superadmin'
324 SUPER_ADMIN = 'superadmin'
325
325
326 REPO_USER = 'user:%s'
326 REPO_USER = 'user:%s'
327 REPO_USERGROUP = 'usergroup:%s'
327 REPO_USERGROUP = 'usergroup:%s'
328 REPO_OWNER = 'repo.owner'
328 REPO_OWNER = 'repo.owner'
329 REPO_DEFAULT = 'repo.default'
329 REPO_DEFAULT = 'repo.default'
330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
331 REPO_PRIVATE = 'repo.private'
331 REPO_PRIVATE = 'repo.private'
332
332
333 REPOGROUP_USER = 'user:%s'
333 REPOGROUP_USER = 'user:%s'
334 REPOGROUP_USERGROUP = 'usergroup:%s'
334 REPOGROUP_USERGROUP = 'usergroup:%s'
335 REPOGROUP_OWNER = 'group.owner'
335 REPOGROUP_OWNER = 'group.owner'
336 REPOGROUP_DEFAULT = 'group.default'
336 REPOGROUP_DEFAULT = 'group.default'
337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
338
338
339 USERGROUP_USER = 'user:%s'
339 USERGROUP_USER = 'user:%s'
340 USERGROUP_USERGROUP = 'usergroup:%s'
340 USERGROUP_USERGROUP = 'usergroup:%s'
341 USERGROUP_OWNER = 'usergroup.owner'
341 USERGROUP_OWNER = 'usergroup.owner'
342 USERGROUP_DEFAULT = 'usergroup.default'
342 USERGROUP_DEFAULT = 'usergroup.default'
343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
344
344
345
345
346 class PermOriginDict(dict):
346 class PermOriginDict(dict):
347 """
347 """
348 A special dict used for tracking permissions along with their origins.
348 A special dict used for tracking permissions along with their origins.
349
349
350 `__setitem__` has been overridden to expect a tuple(perm, origin)
350 `__setitem__` has been overridden to expect a tuple(perm, origin)
351 `__getitem__` will return only the perm
351 `__getitem__` will return only the perm
352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
353
353
354 >>> perms = PermOriginDict()
354 >>> perms = PermOriginDict()
355 >>> perms['resource'] = 'read', 'default'
355 >>> perms['resource'] = 'read', 'default'
356 >>> perms['resource']
356 >>> perms['resource']
357 'read'
357 'read'
358 >>> perms['resource'] = 'write', 'admin'
358 >>> perms['resource'] = 'write', 'admin'
359 >>> perms['resource']
359 >>> perms['resource']
360 'write'
360 'write'
361 >>> perms.perm_origin_stack
361 >>> perms.perm_origin_stack
362 {'resource': [('read', 'default'), ('write', 'admin')]}
362 {'resource': [('read', 'default'), ('write', 'admin')]}
363 """
363 """
364
364
365 def __init__(self, *args, **kw):
365 def __init__(self, *args, **kw):
366 dict.__init__(self, *args, **kw)
366 dict.__init__(self, *args, **kw)
367 self.perm_origin_stack = collections.OrderedDict()
367 self.perm_origin_stack = collections.OrderedDict()
368
368
369 def __setitem__(self, key, (perm, origin)):
369 def __setitem__(self, key, (perm, origin)):
370 self.perm_origin_stack.setdefault(key, []).append(
370 self.perm_origin_stack.setdefault(key, []).append(
371 (perm, origin))
371 (perm, origin))
372 dict.__setitem__(self, key, perm)
372 dict.__setitem__(self, key, perm)
373
373
374
374
375 class BranchPermOriginDict(PermOriginDict):
375 class BranchPermOriginDict(PermOriginDict):
376 """
376 """
377 Dedicated branch permissions dict, with tracking of patterns and origins.
377 Dedicated branch permissions dict, with tracking of patterns and origins.
378
378
379 >>> perms = BranchPermOriginDict()
379 >>> perms = BranchPermOriginDict()
380 >>> perms['resource'] = '*pattern', 'read', 'default'
380 >>> perms['resource'] = '*pattern', 'read', 'default'
381 >>> perms['resource']
381 >>> perms['resource']
382 {'*pattern': 'read'}
382 {'*pattern': 'read'}
383 >>> perms['resource'] = '*pattern', 'write', 'admin'
383 >>> perms['resource'] = '*pattern', 'write', 'admin'
384 >>> perms['resource']
384 >>> perms['resource']
385 {'*pattern': 'write'}
385 {'*pattern': 'write'}
386 >>> perms.perm_origin_stack
386 >>> perms.perm_origin_stack
387 {'resource': {'*pattern': [('read', 'default'), ('write', 'admin')]}}
387 {'resource': {'*pattern': [('read', 'default'), ('write', 'admin')]}}
388 """
388 """
389 def __setitem__(self, key, (pattern, perm, origin)):
389 def __setitem__(self, key, (pattern, perm, origin)):
390
390
391 self.perm_origin_stack.setdefault(key, {}) \
391 self.perm_origin_stack.setdefault(key, {}) \
392 .setdefault(pattern, []).append((perm, origin))
392 .setdefault(pattern, []).append((perm, origin))
393
393
394 if key in self:
394 if key in self:
395 self[key].__setitem__(pattern, perm)
395 self[key].__setitem__(pattern, perm)
396 else:
396 else:
397 patterns = collections.OrderedDict()
397 patterns = collections.OrderedDict()
398 patterns[pattern] = perm
398 patterns[pattern] = perm
399 dict.__setitem__(self, key, patterns)
399 dict.__setitem__(self, key, patterns)
400
400
401
401
402 class PermissionCalculator(object):
402 class PermissionCalculator(object):
403
403
404 def __init__(
404 def __init__(
405 self, user_id, scope, user_is_admin,
405 self, user_id, scope, user_is_admin,
406 user_inherit_default_permissions, explicit, algo,
406 user_inherit_default_permissions, explicit, algo,
407 calculate_super_admin_as_user=False):
407 calculate_super_admin_as_user=False):
408
408
409 self.user_id = user_id
409 self.user_id = user_id
410 self.user_is_admin = user_is_admin
410 self.user_is_admin = user_is_admin
411 self.inherit_default_permissions = user_inherit_default_permissions
411 self.inherit_default_permissions = user_inherit_default_permissions
412 self.explicit = explicit
412 self.explicit = explicit
413 self.algo = algo
413 self.algo = algo
414 self.calculate_super_admin_as_user = calculate_super_admin_as_user
414 self.calculate_super_admin_as_user = calculate_super_admin_as_user
415
415
416 scope = scope or {}
416 scope = scope or {}
417 self.scope_repo_id = scope.get('repo_id')
417 self.scope_repo_id = scope.get('repo_id')
418 self.scope_repo_group_id = scope.get('repo_group_id')
418 self.scope_repo_group_id = scope.get('repo_group_id')
419 self.scope_user_group_id = scope.get('user_group_id')
419 self.scope_user_group_id = scope.get('user_group_id')
420
420
421 self.default_user_id = User.get_default_user(cache=True).user_id
421 self.default_user_id = User.get_default_user(cache=True).user_id
422
422
423 self.permissions_repositories = PermOriginDict()
423 self.permissions_repositories = PermOriginDict()
424 self.permissions_repository_groups = PermOriginDict()
424 self.permissions_repository_groups = PermOriginDict()
425 self.permissions_user_groups = PermOriginDict()
425 self.permissions_user_groups = PermOriginDict()
426 self.permissions_repository_branches = BranchPermOriginDict()
426 self.permissions_repository_branches = BranchPermOriginDict()
427 self.permissions_global = set()
427 self.permissions_global = set()
428
428
429 self.default_repo_perms = Permission.get_default_repo_perms(
429 self.default_repo_perms = Permission.get_default_repo_perms(
430 self.default_user_id, self.scope_repo_id)
430 self.default_user_id, self.scope_repo_id)
431 self.default_repo_groups_perms = Permission.get_default_group_perms(
431 self.default_repo_groups_perms = Permission.get_default_group_perms(
432 self.default_user_id, self.scope_repo_group_id)
432 self.default_user_id, self.scope_repo_group_id)
433 self.default_user_group_perms = \
433 self.default_user_group_perms = \
434 Permission.get_default_user_group_perms(
434 Permission.get_default_user_group_perms(
435 self.default_user_id, self.scope_user_group_id)
435 self.default_user_id, self.scope_user_group_id)
436
436
437 # default branch perms
437 # default branch perms
438 self.default_branch_repo_perms = \
438 self.default_branch_repo_perms = \
439 Permission.get_default_repo_branch_perms(
439 Permission.get_default_repo_branch_perms(
440 self.default_user_id, self.scope_repo_id)
440 self.default_user_id, self.scope_repo_id)
441
441
442 def calculate(self):
442 def calculate(self):
443 if self.user_is_admin and not self.calculate_super_admin_as_user:
443 if self.user_is_admin and not self.calculate_super_admin_as_user:
444 return self._calculate_admin_permissions()
444 return self._calculate_admin_permissions()
445
445
446 self._calculate_global_default_permissions()
446 self._calculate_global_default_permissions()
447 self._calculate_global_permissions()
447 self._calculate_global_permissions()
448 self._calculate_default_permissions()
448 self._calculate_default_permissions()
449 self._calculate_repository_permissions()
449 self._calculate_repository_permissions()
450 self._calculate_repository_branch_permissions()
450 self._calculate_repository_branch_permissions()
451 self._calculate_repository_group_permissions()
451 self._calculate_repository_group_permissions()
452 self._calculate_user_group_permissions()
452 self._calculate_user_group_permissions()
453 return self._permission_structure()
453 return self._permission_structure()
454
454
455 def _calculate_admin_permissions(self):
455 def _calculate_admin_permissions(self):
456 """
456 """
457 admin user have all default rights for repositories
457 admin user have all default rights for repositories
458 and groups set to admin
458 and groups set to admin
459 """
459 """
460 self.permissions_global.add('hg.admin')
460 self.permissions_global.add('hg.admin')
461 self.permissions_global.add('hg.create.write_on_repogroup.true')
461 self.permissions_global.add('hg.create.write_on_repogroup.true')
462
462
463 # repositories
463 # repositories
464 for perm in self.default_repo_perms:
464 for perm in self.default_repo_perms:
465 r_k = perm.UserRepoToPerm.repository.repo_name
465 r_k = perm.UserRepoToPerm.repository.repo_name
466 p = 'repository.admin'
466 p = 'repository.admin'
467 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
467 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
468
468
469 # repository groups
469 # repository groups
470 for perm in self.default_repo_groups_perms:
470 for perm in self.default_repo_groups_perms:
471 rg_k = perm.UserRepoGroupToPerm.group.group_name
471 rg_k = perm.UserRepoGroupToPerm.group.group_name
472 p = 'group.admin'
472 p = 'group.admin'
473 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN
473 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN
474
474
475 # user groups
475 # user groups
476 for perm in self.default_user_group_perms:
476 for perm in self.default_user_group_perms:
477 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
477 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
478 p = 'usergroup.admin'
478 p = 'usergroup.admin'
479 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN
479 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN
480
480
481 # branch permissions
481 # branch permissions
482 # since super-admin also can have custom rule permissions
482 # since super-admin also can have custom rule permissions
483 # we *always* need to calculate those inherited from default, and also explicit
483 # we *always* need to calculate those inherited from default, and also explicit
484 self._calculate_default_permissions_repository_branches(
484 self._calculate_default_permissions_repository_branches(
485 user_inherit_object_permissions=False)
485 user_inherit_object_permissions=False)
486 self._calculate_repository_branch_permissions()
486 self._calculate_repository_branch_permissions()
487
487
488 return self._permission_structure()
488 return self._permission_structure()
489
489
490 def _calculate_global_default_permissions(self):
490 def _calculate_global_default_permissions(self):
491 """
491 """
492 global permissions taken from the default user
492 global permissions taken from the default user
493 """
493 """
494 default_global_perms = UserToPerm.query()\
494 default_global_perms = UserToPerm.query()\
495 .filter(UserToPerm.user_id == self.default_user_id)\
495 .filter(UserToPerm.user_id == self.default_user_id)\
496 .options(joinedload(UserToPerm.permission))
496 .options(joinedload(UserToPerm.permission))
497
497
498 for perm in default_global_perms:
498 for perm in default_global_perms:
499 self.permissions_global.add(perm.permission.permission_name)
499 self.permissions_global.add(perm.permission.permission_name)
500
500
501 if self.user_is_admin:
501 if self.user_is_admin:
502 self.permissions_global.add('hg.admin')
502 self.permissions_global.add('hg.admin')
503 self.permissions_global.add('hg.create.write_on_repogroup.true')
503 self.permissions_global.add('hg.create.write_on_repogroup.true')
504
504
505 def _calculate_global_permissions(self):
505 def _calculate_global_permissions(self):
506 """
506 """
507 Set global system permissions with user permissions or permissions
507 Set global system permissions with user permissions or permissions
508 taken from the user groups of the current user.
508 taken from the user groups of the current user.
509
509
510 The permissions include repo creating, repo group creating, forking
510 The permissions include repo creating, repo group creating, forking
511 etc.
511 etc.
512 """
512 """
513
513
514 # now we read the defined permissions and overwrite what we have set
514 # now we read the defined permissions and overwrite what we have set
515 # before those can be configured from groups or users explicitly.
515 # before those can be configured from groups or users explicitly.
516
516
517 # In case we want to extend this list we should make sure
517 # In case we want to extend this list we should make sure
518 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
518 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
519 _configurable = frozenset([
519 _configurable = frozenset([
520 'hg.fork.none', 'hg.fork.repository',
520 'hg.fork.none', 'hg.fork.repository',
521 'hg.create.none', 'hg.create.repository',
521 'hg.create.none', 'hg.create.repository',
522 'hg.usergroup.create.false', 'hg.usergroup.create.true',
522 'hg.usergroup.create.false', 'hg.usergroup.create.true',
523 'hg.repogroup.create.false', 'hg.repogroup.create.true',
523 'hg.repogroup.create.false', 'hg.repogroup.create.true',
524 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
524 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
525 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
525 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
526 ])
526 ])
527
527
528 # USER GROUPS comes first user group global permissions
528 # USER GROUPS comes first user group global permissions
529 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
529 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
530 .options(joinedload(UserGroupToPerm.permission))\
530 .options(joinedload(UserGroupToPerm.permission))\
531 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
531 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
532 UserGroupMember.users_group_id))\
532 UserGroupMember.users_group_id))\
533 .filter(UserGroupMember.user_id == self.user_id)\
533 .filter(UserGroupMember.user_id == self.user_id)\
534 .order_by(UserGroupToPerm.users_group_id)\
534 .order_by(UserGroupToPerm.users_group_id)\
535 .all()
535 .all()
536
536
537 # need to group here by groups since user can be in more than
537 # need to group here by groups since user can be in more than
538 # one group, so we get all groups
538 # one group, so we get all groups
539 _explicit_grouped_perms = [
539 _explicit_grouped_perms = [
540 [x, list(y)] for x, y in
540 [x, list(y)] for x, y in
541 itertools.groupby(user_perms_from_users_groups,
541 itertools.groupby(user_perms_from_users_groups,
542 lambda _x: _x.users_group)]
542 lambda _x: _x.users_group)]
543
543
544 for gr, perms in _explicit_grouped_perms:
544 for gr, perms in _explicit_grouped_perms:
545 # since user can be in multiple groups iterate over them and
545 # since user can be in multiple groups iterate over them and
546 # select the lowest permissions first (more explicit)
546 # select the lowest permissions first (more explicit)
547 # TODO(marcink): do this^^
547 # TODO(marcink): do this^^
548
548
549 # group doesn't inherit default permissions so we actually set them
549 # group doesn't inherit default permissions so we actually set them
550 if not gr.inherit_default_permissions:
550 if not gr.inherit_default_permissions:
551 # NEED TO IGNORE all previously set configurable permissions
551 # NEED TO IGNORE all previously set configurable permissions
552 # and replace them with explicitly set from this user
552 # and replace them with explicitly set from this user
553 # group permissions
553 # group permissions
554 self.permissions_global = self.permissions_global.difference(
554 self.permissions_global = self.permissions_global.difference(
555 _configurable)
555 _configurable)
556 for perm in perms:
556 for perm in perms:
557 self.permissions_global.add(perm.permission.permission_name)
557 self.permissions_global.add(perm.permission.permission_name)
558
558
559 # user explicit global permissions
559 # user explicit global permissions
560 user_perms = Session().query(UserToPerm)\
560 user_perms = Session().query(UserToPerm)\
561 .options(joinedload(UserToPerm.permission))\
561 .options(joinedload(UserToPerm.permission))\
562 .filter(UserToPerm.user_id == self.user_id).all()
562 .filter(UserToPerm.user_id == self.user_id).all()
563
563
564 if not self.inherit_default_permissions:
564 if not self.inherit_default_permissions:
565 # NEED TO IGNORE all configurable permissions and
565 # NEED TO IGNORE all configurable permissions and
566 # replace them with explicitly set from this user permissions
566 # replace them with explicitly set from this user permissions
567 self.permissions_global = self.permissions_global.difference(
567 self.permissions_global = self.permissions_global.difference(
568 _configurable)
568 _configurable)
569 for perm in user_perms:
569 for perm in user_perms:
570 self.permissions_global.add(perm.permission.permission_name)
570 self.permissions_global.add(perm.permission.permission_name)
571
571
572 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
572 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
573 for perm in self.default_repo_perms:
573 for perm in self.default_repo_perms:
574 r_k = perm.UserRepoToPerm.repository.repo_name
574 r_k = perm.UserRepoToPerm.repository.repo_name
575 p = perm.Permission.permission_name
575 p = perm.Permission.permission_name
576 o = PermOrigin.REPO_DEFAULT
576 o = PermOrigin.REPO_DEFAULT
577 self.permissions_repositories[r_k] = p, o
577 self.permissions_repositories[r_k] = p, o
578
578
579 # if we decide this user isn't inheriting permissions from
579 # if we decide this user isn't inheriting permissions from
580 # default user we set him to .none so only explicit
580 # default user we set him to .none so only explicit
581 # permissions work
581 # permissions work
582 if not user_inherit_object_permissions:
582 if not user_inherit_object_permissions:
583 p = 'repository.none'
583 p = 'repository.none'
584 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
584 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
585 self.permissions_repositories[r_k] = p, o
585 self.permissions_repositories[r_k] = p, o
586
586
587 if perm.Repository.private and not (
587 if perm.Repository.private and not (
588 perm.Repository.user_id == self.user_id):
588 perm.Repository.user_id == self.user_id):
589 # disable defaults for private repos,
589 # disable defaults for private repos,
590 p = 'repository.none'
590 p = 'repository.none'
591 o = PermOrigin.REPO_PRIVATE
591 o = PermOrigin.REPO_PRIVATE
592 self.permissions_repositories[r_k] = p, o
592 self.permissions_repositories[r_k] = p, o
593
593
594 elif perm.Repository.user_id == self.user_id:
594 elif perm.Repository.user_id == self.user_id:
595 # set admin if owner
595 # set admin if owner
596 p = 'repository.admin'
596 p = 'repository.admin'
597 o = PermOrigin.REPO_OWNER
597 o = PermOrigin.REPO_OWNER
598 self.permissions_repositories[r_k] = p, o
598 self.permissions_repositories[r_k] = p, o
599
599
600 if self.user_is_admin:
600 if self.user_is_admin:
601 p = 'repository.admin'
601 p = 'repository.admin'
602 o = PermOrigin.SUPER_ADMIN
602 o = PermOrigin.SUPER_ADMIN
603 self.permissions_repositories[r_k] = p, o
603 self.permissions_repositories[r_k] = p, o
604
604
605 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
605 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
606 for perm in self.default_branch_repo_perms:
606 for perm in self.default_branch_repo_perms:
607
607
608 r_k = perm.UserRepoToPerm.repository.repo_name
608 r_k = perm.UserRepoToPerm.repository.repo_name
609 p = perm.Permission.permission_name
609 p = perm.Permission.permission_name
610 pattern = perm.UserToRepoBranchPermission.branch_pattern
610 pattern = perm.UserToRepoBranchPermission.branch_pattern
611 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
611 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
612
612
613 if not self.explicit:
613 if not self.explicit:
614 # TODO(marcink): fix this for multiple entries
614 # TODO(marcink): fix this for multiple entries
615 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
615 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
616 p = self._choose_permission(p, cur_perm)
616 p = self._choose_permission(p, cur_perm)
617
617
618 # NOTE(marcink): register all pattern/perm instances in this
618 # NOTE(marcink): register all pattern/perm instances in this
619 # special dict that aggregates entries
619 # special dict that aggregates entries
620 self.permissions_repository_branches[r_k] = pattern, p, o
620 self.permissions_repository_branches[r_k] = pattern, p, o
621
621
622 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
622 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
623 for perm in self.default_repo_groups_perms:
623 for perm in self.default_repo_groups_perms:
624 rg_k = perm.UserRepoGroupToPerm.group.group_name
624 rg_k = perm.UserRepoGroupToPerm.group.group_name
625 p = perm.Permission.permission_name
625 p = perm.Permission.permission_name
626 o = PermOrigin.REPOGROUP_DEFAULT
626 o = PermOrigin.REPOGROUP_DEFAULT
627 self.permissions_repository_groups[rg_k] = p, o
627 self.permissions_repository_groups[rg_k] = p, o
628
628
629 # if we decide this user isn't inheriting permissions from default
629 # if we decide this user isn't inheriting permissions from default
630 # user we set him to .none so only explicit permissions work
630 # user we set him to .none so only explicit permissions work
631 if not user_inherit_object_permissions:
631 if not user_inherit_object_permissions:
632 p = 'group.none'
632 p = 'group.none'
633 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
633 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
634 self.permissions_repository_groups[rg_k] = p, o
634 self.permissions_repository_groups[rg_k] = p, o
635
635
636 if perm.RepoGroup.user_id == self.user_id:
636 if perm.RepoGroup.user_id == self.user_id:
637 # set admin if owner
637 # set admin if owner
638 p = 'group.admin'
638 p = 'group.admin'
639 o = PermOrigin.REPOGROUP_OWNER
639 o = PermOrigin.REPOGROUP_OWNER
640 self.permissions_repository_groups[rg_k] = p, o
640 self.permissions_repository_groups[rg_k] = p, o
641
641
642 if self.user_is_admin:
642 if self.user_is_admin:
643 p = 'group.admin'
643 p = 'group.admin'
644 o = PermOrigin.SUPER_ADMIN
644 o = PermOrigin.SUPER_ADMIN
645 self.permissions_repository_groups[rg_k] = p, o
645 self.permissions_repository_groups[rg_k] = p, o
646
646
647 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
647 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
648 for perm in self.default_user_group_perms:
648 for perm in self.default_user_group_perms:
649 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
649 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
650 p = perm.Permission.permission_name
650 p = perm.Permission.permission_name
651 o = PermOrigin.USERGROUP_DEFAULT
651 o = PermOrigin.USERGROUP_DEFAULT
652 self.permissions_user_groups[u_k] = p, o
652 self.permissions_user_groups[u_k] = p, o
653
653
654 # if we decide this user isn't inheriting permissions from default
654 # if we decide this user isn't inheriting permissions from default
655 # user we set him to .none so only explicit permissions work
655 # user we set him to .none so only explicit permissions work
656 if not user_inherit_object_permissions:
656 if not user_inherit_object_permissions:
657 p = 'usergroup.none'
657 p = 'usergroup.none'
658 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
658 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
659 self.permissions_user_groups[u_k] = p, o
659 self.permissions_user_groups[u_k] = p, o
660
660
661 if perm.UserGroup.user_id == self.user_id:
661 if perm.UserGroup.user_id == self.user_id:
662 # set admin if owner
662 # set admin if owner
663 p = 'usergroup.admin'
663 p = 'usergroup.admin'
664 o = PermOrigin.USERGROUP_OWNER
664 o = PermOrigin.USERGROUP_OWNER
665 self.permissions_user_groups[u_k] = p, o
665 self.permissions_user_groups[u_k] = p, o
666
666
667 if self.user_is_admin:
667 if self.user_is_admin:
668 p = 'usergroup.admin'
668 p = 'usergroup.admin'
669 o = PermOrigin.SUPER_ADMIN
669 o = PermOrigin.SUPER_ADMIN
670 self.permissions_user_groups[u_k] = p, o
670 self.permissions_user_groups[u_k] = p, o
671
671
672 def _calculate_default_permissions(self):
672 def _calculate_default_permissions(self):
673 """
673 """
674 Set default user permissions for repositories, repository branches,
674 Set default user permissions for repositories, repository branches,
675 repository groups, user groups taken from the default user.
675 repository groups, user groups taken from the default user.
676
676
677 Calculate inheritance of object permissions based on what we have now
677 Calculate inheritance of object permissions based on what we have now
678 in GLOBAL permissions. We check if .false is in GLOBAL since this is
678 in GLOBAL permissions. We check if .false is in GLOBAL since this is
679 explicitly set. Inherit is the opposite of .false being there.
679 explicitly set. Inherit is the opposite of .false being there.
680
680
681 .. note::
681 .. note::
682
682
683 the syntax is little bit odd but what we need to check here is
683 the syntax is little bit odd but what we need to check here is
684 the opposite of .false permission being in the list so even for
684 the opposite of .false permission being in the list so even for
685 inconsistent state when both .true/.false is there
685 inconsistent state when both .true/.false is there
686 .false is more important
686 .false is more important
687
687
688 """
688 """
689 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
689 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
690 in self.permissions_global)
690 in self.permissions_global)
691
691
692 # default permissions inherited from `default` user permissions
692 # default permissions inherited from `default` user permissions
693 self._calculate_default_permissions_repositories(
693 self._calculate_default_permissions_repositories(
694 user_inherit_object_permissions)
694 user_inherit_object_permissions)
695
695
696 self._calculate_default_permissions_repository_branches(
696 self._calculate_default_permissions_repository_branches(
697 user_inherit_object_permissions)
697 user_inherit_object_permissions)
698
698
699 self._calculate_default_permissions_repository_groups(
699 self._calculate_default_permissions_repository_groups(
700 user_inherit_object_permissions)
700 user_inherit_object_permissions)
701
701
702 self._calculate_default_permissions_user_groups(
702 self._calculate_default_permissions_user_groups(
703 user_inherit_object_permissions)
703 user_inherit_object_permissions)
704
704
705 def _calculate_repository_permissions(self):
705 def _calculate_repository_permissions(self):
706 """
706 """
707 Repository permissions for the current user.
707 Repository permissions for the current user.
708
708
709 Check if the user is part of user groups for this repository and
709 Check if the user is part of user groups for this repository and
710 fill in the permission from it. `_choose_permission` decides of which
710 fill in the permission from it. `_choose_permission` decides of which
711 permission should be selected based on selected method.
711 permission should be selected based on selected method.
712 """
712 """
713
713
714 # user group for repositories permissions
714 # user group for repositories permissions
715 user_repo_perms_from_user_group = Permission\
715 user_repo_perms_from_user_group = Permission\
716 .get_default_repo_perms_from_user_group(
716 .get_default_repo_perms_from_user_group(
717 self.user_id, self.scope_repo_id)
717 self.user_id, self.scope_repo_id)
718
718
719 multiple_counter = collections.defaultdict(int)
719 multiple_counter = collections.defaultdict(int)
720 for perm in user_repo_perms_from_user_group:
720 for perm in user_repo_perms_from_user_group:
721 r_k = perm.UserGroupRepoToPerm.repository.repo_name
721 r_k = perm.UserGroupRepoToPerm.repository.repo_name
722 multiple_counter[r_k] += 1
722 multiple_counter[r_k] += 1
723 p = perm.Permission.permission_name
723 p = perm.Permission.permission_name
724 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
724 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
725 .users_group.users_group_name
725 .users_group.users_group_name
726
726
727 if multiple_counter[r_k] > 1:
727 if multiple_counter[r_k] > 1:
728 cur_perm = self.permissions_repositories[r_k]
728 cur_perm = self.permissions_repositories[r_k]
729 p = self._choose_permission(p, cur_perm)
729 p = self._choose_permission(p, cur_perm)
730
730
731 self.permissions_repositories[r_k] = p, o
731 self.permissions_repositories[r_k] = p, o
732
732
733 if perm.Repository.user_id == self.user_id:
733 if perm.Repository.user_id == self.user_id:
734 # set admin if owner
734 # set admin if owner
735 p = 'repository.admin'
735 p = 'repository.admin'
736 o = PermOrigin.REPO_OWNER
736 o = PermOrigin.REPO_OWNER
737 self.permissions_repositories[r_k] = p, o
737 self.permissions_repositories[r_k] = p, o
738
738
739 if self.user_is_admin:
739 if self.user_is_admin:
740 p = 'repository.admin'
740 p = 'repository.admin'
741 o = PermOrigin.SUPER_ADMIN
741 o = PermOrigin.SUPER_ADMIN
742 self.permissions_repositories[r_k] = p, o
742 self.permissions_repositories[r_k] = p, o
743
743
744 # user explicit permissions for repositories, overrides any specified
744 # user explicit permissions for repositories, overrides any specified
745 # by the group permission
745 # by the group permission
746 user_repo_perms = Permission.get_default_repo_perms(
746 user_repo_perms = Permission.get_default_repo_perms(
747 self.user_id, self.scope_repo_id)
747 self.user_id, self.scope_repo_id)
748 for perm in user_repo_perms:
748 for perm in user_repo_perms:
749 r_k = perm.UserRepoToPerm.repository.repo_name
749 r_k = perm.UserRepoToPerm.repository.repo_name
750 p = perm.Permission.permission_name
750 p = perm.Permission.permission_name
751 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
751 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
752
752
753 if not self.explicit:
753 if not self.explicit:
754 cur_perm = self.permissions_repositories.get(
754 cur_perm = self.permissions_repositories.get(
755 r_k, 'repository.none')
755 r_k, 'repository.none')
756 p = self._choose_permission(p, cur_perm)
756 p = self._choose_permission(p, cur_perm)
757
757
758 self.permissions_repositories[r_k] = p, o
758 self.permissions_repositories[r_k] = p, o
759
759
760 if perm.Repository.user_id == self.user_id:
760 if perm.Repository.user_id == self.user_id:
761 # set admin if owner
761 # set admin if owner
762 p = 'repository.admin'
762 p = 'repository.admin'
763 o = PermOrigin.REPO_OWNER
763 o = PermOrigin.REPO_OWNER
764 self.permissions_repositories[r_k] = p, o
764 self.permissions_repositories[r_k] = p, o
765
765
766 if self.user_is_admin:
766 if self.user_is_admin:
767 p = 'repository.admin'
767 p = 'repository.admin'
768 o = PermOrigin.SUPER_ADMIN
768 o = PermOrigin.SUPER_ADMIN
769 self.permissions_repositories[r_k] = p, o
769 self.permissions_repositories[r_k] = p, o
770
770
771 def _calculate_repository_branch_permissions(self):
771 def _calculate_repository_branch_permissions(self):
772 # user group for repositories permissions
772 # user group for repositories permissions
773 user_repo_branch_perms_from_user_group = Permission\
773 user_repo_branch_perms_from_user_group = Permission\
774 .get_default_repo_branch_perms_from_user_group(
774 .get_default_repo_branch_perms_from_user_group(
775 self.user_id, self.scope_repo_id)
775 self.user_id, self.scope_repo_id)
776
776
777 multiple_counter = collections.defaultdict(int)
777 multiple_counter = collections.defaultdict(int)
778 for perm in user_repo_branch_perms_from_user_group:
778 for perm in user_repo_branch_perms_from_user_group:
779 r_k = perm.UserGroupRepoToPerm.repository.repo_name
779 r_k = perm.UserGroupRepoToPerm.repository.repo_name
780 p = perm.Permission.permission_name
780 p = perm.Permission.permission_name
781 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
781 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
782 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
782 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
783 .users_group.users_group_name
783 .users_group.users_group_name
784
784
785 multiple_counter[r_k] += 1
785 multiple_counter[r_k] += 1
786 if multiple_counter[r_k] > 1:
786 if multiple_counter[r_k] > 1:
787 # TODO(marcink): fix this for multi branch support, and multiple entries
787 # TODO(marcink): fix this for multi branch support, and multiple entries
788 cur_perm = self.permissions_repository_branches[r_k]
788 cur_perm = self.permissions_repository_branches[r_k]
789 p = self._choose_permission(p, cur_perm)
789 p = self._choose_permission(p, cur_perm)
790
790
791 self.permissions_repository_branches[r_k] = pattern, p, o
791 self.permissions_repository_branches[r_k] = pattern, p, o
792
792
793 # user explicit branch permissions for repositories, overrides
793 # user explicit branch permissions for repositories, overrides
794 # any specified by the group permission
794 # any specified by the group permission
795 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
795 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
796 self.user_id, self.scope_repo_id)
796 self.user_id, self.scope_repo_id)
797
797
798 for perm in user_repo_branch_perms:
798 for perm in user_repo_branch_perms:
799
799
800 r_k = perm.UserRepoToPerm.repository.repo_name
800 r_k = perm.UserRepoToPerm.repository.repo_name
801 p = perm.Permission.permission_name
801 p = perm.Permission.permission_name
802 pattern = perm.UserToRepoBranchPermission.branch_pattern
802 pattern = perm.UserToRepoBranchPermission.branch_pattern
803 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
803 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
804
804
805 if not self.explicit:
805 if not self.explicit:
806 # TODO(marcink): fix this for multiple entries
806 # TODO(marcink): fix this for multiple entries
807 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
807 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
808 p = self._choose_permission(p, cur_perm)
808 p = self._choose_permission(p, cur_perm)
809
809
810 # NOTE(marcink): register all pattern/perm instances in this
810 # NOTE(marcink): register all pattern/perm instances in this
811 # special dict that aggregates entries
811 # special dict that aggregates entries
812 self.permissions_repository_branches[r_k] = pattern, p, o
812 self.permissions_repository_branches[r_k] = pattern, p, o
813
813
814 def _calculate_repository_group_permissions(self):
814 def _calculate_repository_group_permissions(self):
815 """
815 """
816 Repository group permissions for the current user.
816 Repository group permissions for the current user.
817
817
818 Check if the user is part of user groups for repository groups and
818 Check if the user is part of user groups for repository groups and
819 fill in the permissions from it. `_choose_permission` decides of which
819 fill in the permissions from it. `_choose_permission` decides of which
820 permission should be selected based on selected method.
820 permission should be selected based on selected method.
821 """
821 """
822 # user group for repo groups permissions
822 # user group for repo groups permissions
823 user_repo_group_perms_from_user_group = Permission\
823 user_repo_group_perms_from_user_group = Permission\
824 .get_default_group_perms_from_user_group(
824 .get_default_group_perms_from_user_group(
825 self.user_id, self.scope_repo_group_id)
825 self.user_id, self.scope_repo_group_id)
826
826
827 multiple_counter = collections.defaultdict(int)
827 multiple_counter = collections.defaultdict(int)
828 for perm in user_repo_group_perms_from_user_group:
828 for perm in user_repo_group_perms_from_user_group:
829 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
829 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
830 multiple_counter[rg_k] += 1
830 multiple_counter[rg_k] += 1
831 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
831 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
832 .users_group.users_group_name
832 .users_group.users_group_name
833 p = perm.Permission.permission_name
833 p = perm.Permission.permission_name
834
834
835 if multiple_counter[rg_k] > 1:
835 if multiple_counter[rg_k] > 1:
836 cur_perm = self.permissions_repository_groups[rg_k]
836 cur_perm = self.permissions_repository_groups[rg_k]
837 p = self._choose_permission(p, cur_perm)
837 p = self._choose_permission(p, cur_perm)
838 self.permissions_repository_groups[rg_k] = p, o
838 self.permissions_repository_groups[rg_k] = p, o
839
839
840 if perm.RepoGroup.user_id == self.user_id:
840 if perm.RepoGroup.user_id == self.user_id:
841 # set admin if owner, even for member of other user group
841 # set admin if owner, even for member of other user group
842 p = 'group.admin'
842 p = 'group.admin'
843 o = PermOrigin.REPOGROUP_OWNER
843 o = PermOrigin.REPOGROUP_OWNER
844 self.permissions_repository_groups[rg_k] = p, o
844 self.permissions_repository_groups[rg_k] = p, o
845
845
846 if self.user_is_admin:
846 if self.user_is_admin:
847 p = 'group.admin'
847 p = 'group.admin'
848 o = PermOrigin.SUPER_ADMIN
848 o = PermOrigin.SUPER_ADMIN
849 self.permissions_repository_groups[rg_k] = p, o
849 self.permissions_repository_groups[rg_k] = p, o
850
850
851 # user explicit permissions for repository groups
851 # user explicit permissions for repository groups
852 user_repo_groups_perms = Permission.get_default_group_perms(
852 user_repo_groups_perms = Permission.get_default_group_perms(
853 self.user_id, self.scope_repo_group_id)
853 self.user_id, self.scope_repo_group_id)
854 for perm in user_repo_groups_perms:
854 for perm in user_repo_groups_perms:
855 rg_k = perm.UserRepoGroupToPerm.group.group_name
855 rg_k = perm.UserRepoGroupToPerm.group.group_name
856 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
856 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
857 .user.username
857 .user.username
858 p = perm.Permission.permission_name
858 p = perm.Permission.permission_name
859
859
860 if not self.explicit:
860 if not self.explicit:
861 cur_perm = self.permissions_repository_groups.get(
861 cur_perm = self.permissions_repository_groups.get(
862 rg_k, 'group.none')
862 rg_k, 'group.none')
863 p = self._choose_permission(p, cur_perm)
863 p = self._choose_permission(p, cur_perm)
864
864
865 self.permissions_repository_groups[rg_k] = p, o
865 self.permissions_repository_groups[rg_k] = p, o
866
866
867 if perm.RepoGroup.user_id == self.user_id:
867 if perm.RepoGroup.user_id == self.user_id:
868 # set admin if owner
868 # set admin if owner
869 p = 'group.admin'
869 p = 'group.admin'
870 o = PermOrigin.REPOGROUP_OWNER
870 o = PermOrigin.REPOGROUP_OWNER
871 self.permissions_repository_groups[rg_k] = p, o
871 self.permissions_repository_groups[rg_k] = p, o
872
872
873 if self.user_is_admin:
873 if self.user_is_admin:
874 p = 'group.admin'
874 p = 'group.admin'
875 o = PermOrigin.SUPER_ADMIN
875 o = PermOrigin.SUPER_ADMIN
876 self.permissions_repository_groups[rg_k] = p, o
876 self.permissions_repository_groups[rg_k] = p, o
877
877
878 def _calculate_user_group_permissions(self):
878 def _calculate_user_group_permissions(self):
879 """
879 """
880 User group permissions for the current user.
880 User group permissions for the current user.
881 """
881 """
882 # user group for user group permissions
882 # user group for user group permissions
883 user_group_from_user_group = Permission\
883 user_group_from_user_group = Permission\
884 .get_default_user_group_perms_from_user_group(
884 .get_default_user_group_perms_from_user_group(
885 self.user_id, self.scope_user_group_id)
885 self.user_id, self.scope_user_group_id)
886
886
887 multiple_counter = collections.defaultdict(int)
887 multiple_counter = collections.defaultdict(int)
888 for perm in user_group_from_user_group:
888 for perm in user_group_from_user_group:
889 ug_k = perm.UserGroupUserGroupToPerm\
889 ug_k = perm.UserGroupUserGroupToPerm\
890 .target_user_group.users_group_name
890 .target_user_group.users_group_name
891 multiple_counter[ug_k] += 1
891 multiple_counter[ug_k] += 1
892 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
892 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
893 .user_group.users_group_name
893 .user_group.users_group_name
894 p = perm.Permission.permission_name
894 p = perm.Permission.permission_name
895
895
896 if multiple_counter[ug_k] > 1:
896 if multiple_counter[ug_k] > 1:
897 cur_perm = self.permissions_user_groups[ug_k]
897 cur_perm = self.permissions_user_groups[ug_k]
898 p = self._choose_permission(p, cur_perm)
898 p = self._choose_permission(p, cur_perm)
899
899
900 self.permissions_user_groups[ug_k] = p, o
900 self.permissions_user_groups[ug_k] = p, o
901
901
902 if perm.UserGroup.user_id == self.user_id:
902 if perm.UserGroup.user_id == self.user_id:
903 # set admin if owner, even for member of other user group
903 # set admin if owner, even for member of other user group
904 p = 'usergroup.admin'
904 p = 'usergroup.admin'
905 o = PermOrigin.USERGROUP_OWNER
905 o = PermOrigin.USERGROUP_OWNER
906 self.permissions_user_groups[ug_k] = p, o
906 self.permissions_user_groups[ug_k] = p, o
907
907
908 if self.user_is_admin:
908 if self.user_is_admin:
909 p = 'usergroup.admin'
909 p = 'usergroup.admin'
910 o = PermOrigin.SUPER_ADMIN
910 o = PermOrigin.SUPER_ADMIN
911 self.permissions_user_groups[ug_k] = p, o
911 self.permissions_user_groups[ug_k] = p, o
912
912
913 # user explicit permission for user groups
913 # user explicit permission for user groups
914 user_user_groups_perms = Permission.get_default_user_group_perms(
914 user_user_groups_perms = Permission.get_default_user_group_perms(
915 self.user_id, self.scope_user_group_id)
915 self.user_id, self.scope_user_group_id)
916 for perm in user_user_groups_perms:
916 for perm in user_user_groups_perms:
917 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
917 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
918 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
918 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
919 .user.username
919 .user.username
920 p = perm.Permission.permission_name
920 p = perm.Permission.permission_name
921
921
922 if not self.explicit:
922 if not self.explicit:
923 cur_perm = self.permissions_user_groups.get(
923 cur_perm = self.permissions_user_groups.get(
924 ug_k, 'usergroup.none')
924 ug_k, 'usergroup.none')
925 p = self._choose_permission(p, cur_perm)
925 p = self._choose_permission(p, cur_perm)
926
926
927 self.permissions_user_groups[ug_k] = p, o
927 self.permissions_user_groups[ug_k] = p, o
928
928
929 if perm.UserGroup.user_id == self.user_id:
929 if perm.UserGroup.user_id == self.user_id:
930 # set admin if owner
930 # set admin if owner
931 p = 'usergroup.admin'
931 p = 'usergroup.admin'
932 o = PermOrigin.USERGROUP_OWNER
932 o = PermOrigin.USERGROUP_OWNER
933 self.permissions_user_groups[ug_k] = p, o
933 self.permissions_user_groups[ug_k] = p, o
934
934
935 if self.user_is_admin:
935 if self.user_is_admin:
936 p = 'usergroup.admin'
936 p = 'usergroup.admin'
937 o = PermOrigin.SUPER_ADMIN
937 o = PermOrigin.SUPER_ADMIN
938 self.permissions_user_groups[ug_k] = p, o
938 self.permissions_user_groups[ug_k] = p, o
939
939
940 def _choose_permission(self, new_perm, cur_perm):
940 def _choose_permission(self, new_perm, cur_perm):
941 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
941 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
942 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
942 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
943 if self.algo == 'higherwin':
943 if self.algo == 'higherwin':
944 if new_perm_val > cur_perm_val:
944 if new_perm_val > cur_perm_val:
945 return new_perm
945 return new_perm
946 return cur_perm
946 return cur_perm
947 elif self.algo == 'lowerwin':
947 elif self.algo == 'lowerwin':
948 if new_perm_val < cur_perm_val:
948 if new_perm_val < cur_perm_val:
949 return new_perm
949 return new_perm
950 return cur_perm
950 return cur_perm
951
951
952 def _permission_structure(self):
952 def _permission_structure(self):
953 return {
953 return {
954 'global': self.permissions_global,
954 'global': self.permissions_global,
955 'repositories': self.permissions_repositories,
955 'repositories': self.permissions_repositories,
956 'repository_branches': self.permissions_repository_branches,
956 'repository_branches': self.permissions_repository_branches,
957 'repositories_groups': self.permissions_repository_groups,
957 'repositories_groups': self.permissions_repository_groups,
958 'user_groups': self.permissions_user_groups,
958 'user_groups': self.permissions_user_groups,
959 }
959 }
960
960
961
961
962 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
962 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
963 """
963 """
964 Check if given controller_name is in whitelist of auth token access
964 Check if given controller_name is in whitelist of auth token access
965 """
965 """
966 if not whitelist:
966 if not whitelist:
967 from rhodecode import CONFIG
967 from rhodecode import CONFIG
968 whitelist = aslist(
968 whitelist = aslist(
969 CONFIG.get('api_access_controllers_whitelist'), sep=',')
969 CONFIG.get('api_access_controllers_whitelist'), sep=',')
970 # backward compat translation
970 # backward compat translation
971 compat = {
971 compat = {
972 # old controller, new VIEW
972 # old controller, new VIEW
973 'ChangesetController:*': 'RepoCommitsView:*',
973 'ChangesetController:*': 'RepoCommitsView:*',
974 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
974 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
975 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
975 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
976 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
976 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
977 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
977 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
978 'GistsController:*': 'GistView:*',
978 'GistsController:*': 'GistView:*',
979 }
979 }
980
980
981 log.debug(
981 log.debug(
982 'Allowed views for AUTH TOKEN access: %s' % (whitelist,))
982 'Allowed views for AUTH TOKEN access: %s', whitelist)
983 auth_token_access_valid = False
983 auth_token_access_valid = False
984
984
985 for entry in whitelist:
985 for entry in whitelist:
986 token_match = True
986 token_match = True
987 if entry in compat:
987 if entry in compat:
988 # translate from old Controllers to Pyramid Views
988 # translate from old Controllers to Pyramid Views
989 entry = compat[entry]
989 entry = compat[entry]
990
990
991 if '@' in entry:
991 if '@' in entry:
992 # specific AuthToken
992 # specific AuthToken
993 entry, allowed_token = entry.split('@', 1)
993 entry, allowed_token = entry.split('@', 1)
994 token_match = auth_token == allowed_token
994 token_match = auth_token == allowed_token
995
995
996 if fnmatch.fnmatch(view_name, entry) and token_match:
996 if fnmatch.fnmatch(view_name, entry) and token_match:
997 auth_token_access_valid = True
997 auth_token_access_valid = True
998 break
998 break
999
999
1000 if auth_token_access_valid:
1000 if auth_token_access_valid:
1001 log.debug('view: `%s` matches entry in whitelist: %s'
1001 log.debug('view: `%s` matches entry in whitelist: %s',
1002 % (view_name, whitelist))
1002 view_name, whitelist)
1003
1003 else:
1004 else:
1004 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
1005 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
1005 % (view_name, whitelist))
1006 % (view_name, whitelist))
1006 if auth_token:
1007 if auth_token:
1007 # if we use auth token key and don't have access it's a warning
1008 # if we use auth token key and don't have access it's a warning
1008 log.warning(msg)
1009 log.warning(msg)
1009 else:
1010 else:
1010 log.debug(msg)
1011 log.debug(msg)
1011
1012
1012 return auth_token_access_valid
1013 return auth_token_access_valid
1013
1014
1014
1015
1015 class AuthUser(object):
1016 class AuthUser(object):
1016 """
1017 """
1017 A simple object that handles all attributes of user in RhodeCode
1018 A simple object that handles all attributes of user in RhodeCode
1018
1019
1019 It does lookup based on API key,given user, or user present in session
1020 It does lookup based on API key,given user, or user present in session
1020 Then it fills all required information for such user. It also checks if
1021 Then it fills all required information for such user. It also checks if
1021 anonymous access is enabled and if so, it returns default user as logged in
1022 anonymous access is enabled and if so, it returns default user as logged in
1022 """
1023 """
1023 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1024 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1024
1025
1025 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1026 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1026
1027
1027 self.user_id = user_id
1028 self.user_id = user_id
1028 self._api_key = api_key
1029 self._api_key = api_key
1029
1030
1030 self.api_key = None
1031 self.api_key = None
1031 self.username = username
1032 self.username = username
1032 self.ip_addr = ip_addr
1033 self.ip_addr = ip_addr
1033 self.name = ''
1034 self.name = ''
1034 self.lastname = ''
1035 self.lastname = ''
1035 self.first_name = ''
1036 self.first_name = ''
1036 self.last_name = ''
1037 self.last_name = ''
1037 self.email = ''
1038 self.email = ''
1038 self.is_authenticated = False
1039 self.is_authenticated = False
1039 self.admin = False
1040 self.admin = False
1040 self.inherit_default_permissions = False
1041 self.inherit_default_permissions = False
1041 self.password = ''
1042 self.password = ''
1042
1043
1043 self.anonymous_user = None # propagated on propagate_data
1044 self.anonymous_user = None # propagated on propagate_data
1044 self.propagate_data()
1045 self.propagate_data()
1045 self._instance = None
1046 self._instance = None
1046 self._permissions_scoped_cache = {} # used to bind scoped calculation
1047 self._permissions_scoped_cache = {} # used to bind scoped calculation
1047
1048
1048 @LazyProperty
1049 @LazyProperty
1049 def permissions(self):
1050 def permissions(self):
1050 return self.get_perms(user=self, cache=None)
1051 return self.get_perms(user=self, cache=None)
1051
1052
1052 @LazyProperty
1053 @LazyProperty
1053 def permissions_safe(self):
1054 def permissions_safe(self):
1054 """
1055 """
1055 Filtered permissions excluding not allowed repositories
1056 Filtered permissions excluding not allowed repositories
1056 """
1057 """
1057 perms = self.get_perms(user=self, cache=None)
1058 perms = self.get_perms(user=self, cache=None)
1058
1059
1059 perms['repositories'] = {
1060 perms['repositories'] = {
1060 k: v for k, v in perms['repositories'].items()
1061 k: v for k, v in perms['repositories'].items()
1061 if v != 'repository.none'}
1062 if v != 'repository.none'}
1062 perms['repositories_groups'] = {
1063 perms['repositories_groups'] = {
1063 k: v for k, v in perms['repositories_groups'].items()
1064 k: v for k, v in perms['repositories_groups'].items()
1064 if v != 'group.none'}
1065 if v != 'group.none'}
1065 perms['user_groups'] = {
1066 perms['user_groups'] = {
1066 k: v for k, v in perms['user_groups'].items()
1067 k: v for k, v in perms['user_groups'].items()
1067 if v != 'usergroup.none'}
1068 if v != 'usergroup.none'}
1068 perms['repository_branches'] = {
1069 perms['repository_branches'] = {
1069 k: v for k, v in perms['repository_branches'].iteritems()
1070 k: v for k, v in perms['repository_branches'].iteritems()
1070 if v != 'branch.none'}
1071 if v != 'branch.none'}
1071 return perms
1072 return perms
1072
1073
1073 @LazyProperty
1074 @LazyProperty
1074 def permissions_full_details(self):
1075 def permissions_full_details(self):
1075 return self.get_perms(
1076 return self.get_perms(
1076 user=self, cache=None, calculate_super_admin=True)
1077 user=self, cache=None, calculate_super_admin=True)
1077
1078
1078 def permissions_with_scope(self, scope):
1079 def permissions_with_scope(self, scope):
1079 """
1080 """
1080 Call the get_perms function with scoped data. The scope in that function
1081 Call the get_perms function with scoped data. The scope in that function
1081 narrows the SQL calls to the given ID of objects resulting in fetching
1082 narrows the SQL calls to the given ID of objects resulting in fetching
1082 Just particular permission we want to obtain. If scope is an empty dict
1083 Just particular permission we want to obtain. If scope is an empty dict
1083 then it basically narrows the scope to GLOBAL permissions only.
1084 then it basically narrows the scope to GLOBAL permissions only.
1084
1085
1085 :param scope: dict
1086 :param scope: dict
1086 """
1087 """
1087 if 'repo_name' in scope:
1088 if 'repo_name' in scope:
1088 obj = Repository.get_by_repo_name(scope['repo_name'])
1089 obj = Repository.get_by_repo_name(scope['repo_name'])
1089 if obj:
1090 if obj:
1090 scope['repo_id'] = obj.repo_id
1091 scope['repo_id'] = obj.repo_id
1091 _scope = collections.OrderedDict()
1092 _scope = collections.OrderedDict()
1092 _scope['repo_id'] = -1
1093 _scope['repo_id'] = -1
1093 _scope['user_group_id'] = -1
1094 _scope['user_group_id'] = -1
1094 _scope['repo_group_id'] = -1
1095 _scope['repo_group_id'] = -1
1095
1096
1096 for k in sorted(scope.keys()):
1097 for k in sorted(scope.keys()):
1097 _scope[k] = scope[k]
1098 _scope[k] = scope[k]
1098
1099
1099 # store in cache to mimic how the @LazyProperty works,
1100 # store in cache to mimic how the @LazyProperty works,
1100 # the difference here is that we use the unique key calculated
1101 # the difference here is that we use the unique key calculated
1101 # from params and values
1102 # from params and values
1102 return self.get_perms(user=self, cache=None, scope=_scope)
1103 return self.get_perms(user=self, cache=None, scope=_scope)
1103
1104
1104 def get_instance(self):
1105 def get_instance(self):
1105 return User.get(self.user_id)
1106 return User.get(self.user_id)
1106
1107
1107 def propagate_data(self):
1108 def propagate_data(self):
1108 """
1109 """
1109 Fills in user data and propagates values to this instance. Maps fetched
1110 Fills in user data and propagates values to this instance. Maps fetched
1110 user attributes to this class instance attributes
1111 user attributes to this class instance attributes
1111 """
1112 """
1112 log.debug('AuthUser: starting data propagation for new potential user')
1113 log.debug('AuthUser: starting data propagation for new potential user')
1113 user_model = UserModel()
1114 user_model = UserModel()
1114 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1115 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1115 is_user_loaded = False
1116 is_user_loaded = False
1116
1117
1117 # lookup by userid
1118 # lookup by userid
1118 if self.user_id is not None and self.user_id != anon_user.user_id:
1119 if self.user_id is not None and self.user_id != anon_user.user_id:
1119 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1120 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1120 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1121 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1121
1122
1122 # try go get user by api key
1123 # try go get user by api key
1123 elif self._api_key and self._api_key != anon_user.api_key:
1124 elif self._api_key and self._api_key != anon_user.api_key:
1124 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1125 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1125 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1126 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1126
1127
1127 # lookup by username
1128 # lookup by username
1128 elif self.username:
1129 elif self.username:
1129 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1130 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1130 is_user_loaded = user_model.fill_data(self, username=self.username)
1131 is_user_loaded = user_model.fill_data(self, username=self.username)
1131 else:
1132 else:
1132 log.debug('No data in %s that could been used to log in', self)
1133 log.debug('No data in %s that could been used to log in', self)
1133
1134
1134 if not is_user_loaded:
1135 if not is_user_loaded:
1135 log.debug(
1136 log.debug(
1136 'Failed to load user. Fallback to default user %s', anon_user)
1137 'Failed to load user. Fallback to default user %s', anon_user)
1137 # if we cannot authenticate user try anonymous
1138 # if we cannot authenticate user try anonymous
1138 if anon_user.active:
1139 if anon_user.active:
1139 log.debug('default user is active, using it as a session user')
1140 log.debug('default user is active, using it as a session user')
1140 user_model.fill_data(self, user_id=anon_user.user_id)
1141 user_model.fill_data(self, user_id=anon_user.user_id)
1141 # then we set this user is logged in
1142 # then we set this user is logged in
1142 self.is_authenticated = True
1143 self.is_authenticated = True
1143 else:
1144 else:
1144 log.debug('default user is NOT active')
1145 log.debug('default user is NOT active')
1145 # in case of disabled anonymous user we reset some of the
1146 # in case of disabled anonymous user we reset some of the
1146 # parameters so such user is "corrupted", skipping the fill_data
1147 # parameters so such user is "corrupted", skipping the fill_data
1147 for attr in ['user_id', 'username', 'admin', 'active']:
1148 for attr in ['user_id', 'username', 'admin', 'active']:
1148 setattr(self, attr, None)
1149 setattr(self, attr, None)
1149 self.is_authenticated = False
1150 self.is_authenticated = False
1150
1151
1151 if not self.username:
1152 if not self.username:
1152 self.username = 'None'
1153 self.username = 'None'
1153
1154
1154 log.debug('AuthUser: propagated user is now %s', self)
1155 log.debug('AuthUser: propagated user is now %s', self)
1155
1156
1156 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1157 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1157 calculate_super_admin=False, cache=None):
1158 calculate_super_admin=False, cache=None):
1158 """
1159 """
1159 Fills user permission attribute with permissions taken from database
1160 Fills user permission attribute with permissions taken from database
1160 works for permissions given for repositories, and for permissions that
1161 works for permissions given for repositories, and for permissions that
1161 are granted to groups
1162 are granted to groups
1162
1163
1163 :param user: instance of User object from database
1164 :param user: instance of User object from database
1164 :param explicit: In case there are permissions both for user and a group
1165 :param explicit: In case there are permissions both for user and a group
1165 that user is part of, explicit flag will defiine if user will
1166 that user is part of, explicit flag will defiine if user will
1166 explicitly override permissions from group, if it's False it will
1167 explicitly override permissions from group, if it's False it will
1167 make decision based on the algo
1168 make decision based on the algo
1168 :param algo: algorithm to decide what permission should be choose if
1169 :param algo: algorithm to decide what permission should be choose if
1169 it's multiple defined, eg user in two different groups. It also
1170 it's multiple defined, eg user in two different groups. It also
1170 decides if explicit flag is turned off how to specify the permission
1171 decides if explicit flag is turned off how to specify the permission
1171 for case when user is in a group + have defined separate permission
1172 for case when user is in a group + have defined separate permission
1172 :param calculate_super_admin: calculate permissions for super-admin in the
1173 :param calculate_super_admin: calculate permissions for super-admin in the
1173 same way as for regular user without speedups
1174 same way as for regular user without speedups
1174 :param cache: Use caching for calculation, None = let the cache backend decide
1175 :param cache: Use caching for calculation, None = let the cache backend decide
1175 """
1176 """
1176 user_id = user.user_id
1177 user_id = user.user_id
1177 user_is_admin = user.is_admin
1178 user_is_admin = user.is_admin
1178
1179
1179 # inheritance of global permissions like create repo/fork repo etc
1180 # inheritance of global permissions like create repo/fork repo etc
1180 user_inherit_default_permissions = user.inherit_default_permissions
1181 user_inherit_default_permissions = user.inherit_default_permissions
1181
1182
1182 cache_seconds = safe_int(
1183 cache_seconds = safe_int(
1183 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1184 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1184
1185
1185 if cache is None:
1186 if cache is None:
1186 # let the backend cache decide
1187 # let the backend cache decide
1187 cache_on = cache_seconds > 0
1188 cache_on = cache_seconds > 0
1188 else:
1189 else:
1189 cache_on = cache
1190 cache_on = cache
1190
1191
1191 log.debug(
1192 log.debug(
1192 'Computing PERMISSION tree for user %s scope `%s` '
1193 'Computing PERMISSION tree for user %s scope `%s` '
1193 'with caching: %s[TTL: %ss]' % (user, scope, cache_on, cache_seconds or 0))
1194 'with caching: %s[TTL: %ss]', user, scope, cache_on, cache_seconds or 0)
1194
1195
1195 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1196 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1196 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1197 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1197
1198
1198 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1199 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1199 condition=cache_on)
1200 condition=cache_on)
1200 def compute_perm_tree(cache_name,
1201 def compute_perm_tree(cache_name,
1201 user_id, scope, user_is_admin,user_inherit_default_permissions,
1202 user_id, scope, user_is_admin,user_inherit_default_permissions,
1202 explicit, algo, calculate_super_admin):
1203 explicit, algo, calculate_super_admin):
1203 return _cached_perms_data(
1204 return _cached_perms_data(
1204 user_id, scope, user_is_admin, user_inherit_default_permissions,
1205 user_id, scope, user_is_admin, user_inherit_default_permissions,
1205 explicit, algo, calculate_super_admin)
1206 explicit, algo, calculate_super_admin)
1206
1207
1207 start = time.time()
1208 start = time.time()
1208 result = compute_perm_tree(
1209 result = compute_perm_tree(
1209 'permissions', user_id, scope, user_is_admin,
1210 'permissions', user_id, scope, user_is_admin,
1210 user_inherit_default_permissions, explicit, algo,
1211 user_inherit_default_permissions, explicit, algo,
1211 calculate_super_admin)
1212 calculate_super_admin)
1212
1213
1213 result_repr = []
1214 result_repr = []
1214 for k in result:
1215 for k in result:
1215 result_repr.append((k, len(result[k])))
1216 result_repr.append((k, len(result[k])))
1216 total = time.time() - start
1217 total = time.time() - start
1217 log.debug('PERMISSION tree for user %s computed in %.3fs: %s' % (
1218 log.debug('PERMISSION tree for user %s computed in %.3fs: %s',
1218 user, total, result_repr))
1219 user, total, result_repr)
1219
1220
1220 return result
1221 return result
1221
1222
1222 @property
1223 @property
1223 def is_default(self):
1224 def is_default(self):
1224 return self.username == User.DEFAULT_USER
1225 return self.username == User.DEFAULT_USER
1225
1226
1226 @property
1227 @property
1227 def is_admin(self):
1228 def is_admin(self):
1228 return self.admin
1229 return self.admin
1229
1230
1230 @property
1231 @property
1231 def is_user_object(self):
1232 def is_user_object(self):
1232 return self.user_id is not None
1233 return self.user_id is not None
1233
1234
1234 @property
1235 @property
1235 def repositories_admin(self):
1236 def repositories_admin(self):
1236 """
1237 """
1237 Returns list of repositories you're an admin of
1238 Returns list of repositories you're an admin of
1238 """
1239 """
1239 return [
1240 return [
1240 x[0] for x in self.permissions['repositories'].items()
1241 x[0] for x in self.permissions['repositories'].items()
1241 if x[1] == 'repository.admin']
1242 if x[1] == 'repository.admin']
1242
1243
1243 @property
1244 @property
1244 def repository_groups_admin(self):
1245 def repository_groups_admin(self):
1245 """
1246 """
1246 Returns list of repository groups you're an admin of
1247 Returns list of repository groups you're an admin of
1247 """
1248 """
1248 return [
1249 return [
1249 x[0] for x in self.permissions['repositories_groups'].items()
1250 x[0] for x in self.permissions['repositories_groups'].items()
1250 if x[1] == 'group.admin']
1251 if x[1] == 'group.admin']
1251
1252
1252 @property
1253 @property
1253 def user_groups_admin(self):
1254 def user_groups_admin(self):
1254 """
1255 """
1255 Returns list of user groups you're an admin of
1256 Returns list of user groups you're an admin of
1256 """
1257 """
1257 return [
1258 return [
1258 x[0] for x in self.permissions['user_groups'].items()
1259 x[0] for x in self.permissions['user_groups'].items()
1259 if x[1] == 'usergroup.admin']
1260 if x[1] == 'usergroup.admin']
1260
1261
1261 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1262 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1262 """
1263 """
1263 Returns list of repository ids that user have access to based on given
1264 Returns list of repository ids that user have access to based on given
1264 perms. The cache flag should be only used in cases that are used for
1265 perms. The cache flag should be only used in cases that are used for
1265 display purposes, NOT IN ANY CASE for permission checks.
1266 display purposes, NOT IN ANY CASE for permission checks.
1266 """
1267 """
1267 from rhodecode.model.scm import RepoList
1268 from rhodecode.model.scm import RepoList
1268 if not perms:
1269 if not perms:
1269 perms = [
1270 perms = [
1270 'repository.read', 'repository.write', 'repository.admin']
1271 'repository.read', 'repository.write', 'repository.admin']
1271
1272
1272 def _cached_repo_acl(user_id, perm_def, _name_filter):
1273 def _cached_repo_acl(user_id, perm_def, _name_filter):
1273 qry = Repository.query()
1274 qry = Repository.query()
1274 if _name_filter:
1275 if _name_filter:
1275 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1276 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1276 qry = qry.filter(
1277 qry = qry.filter(
1277 Repository.repo_name.ilike(ilike_expression))
1278 Repository.repo_name.ilike(ilike_expression))
1278
1279
1279 return [x.repo_id for x in
1280 return [x.repo_id for x in
1280 RepoList(qry, perm_set=perm_def)]
1281 RepoList(qry, perm_set=perm_def)]
1281
1282
1282 return _cached_repo_acl(self.user_id, perms, name_filter)
1283 return _cached_repo_acl(self.user_id, perms, name_filter)
1283
1284
1284 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1285 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1285 """
1286 """
1286 Returns list of repository group ids that user have access to based on given
1287 Returns list of repository group ids that user have access to based on given
1287 perms. The cache flag should be only used in cases that are used for
1288 perms. The cache flag should be only used in cases that are used for
1288 display purposes, NOT IN ANY CASE for permission checks.
1289 display purposes, NOT IN ANY CASE for permission checks.
1289 """
1290 """
1290 from rhodecode.model.scm import RepoGroupList
1291 from rhodecode.model.scm import RepoGroupList
1291 if not perms:
1292 if not perms:
1292 perms = [
1293 perms = [
1293 'group.read', 'group.write', 'group.admin']
1294 'group.read', 'group.write', 'group.admin']
1294
1295
1295 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1296 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1296 qry = RepoGroup.query()
1297 qry = RepoGroup.query()
1297 if _name_filter:
1298 if _name_filter:
1298 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1299 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1299 qry = qry.filter(
1300 qry = qry.filter(
1300 RepoGroup.group_name.ilike(ilike_expression))
1301 RepoGroup.group_name.ilike(ilike_expression))
1301
1302
1302 return [x.group_id for x in
1303 return [x.group_id for x in
1303 RepoGroupList(qry, perm_set=perm_def)]
1304 RepoGroupList(qry, perm_set=perm_def)]
1304
1305
1305 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1306 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1306
1307
1307 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1308 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1308 """
1309 """
1309 Returns list of user group ids that user have access to based on given
1310 Returns list of user group ids that user have access to based on given
1310 perms. The cache flag should be only used in cases that are used for
1311 perms. The cache flag should be only used in cases that are used for
1311 display purposes, NOT IN ANY CASE for permission checks.
1312 display purposes, NOT IN ANY CASE for permission checks.
1312 """
1313 """
1313 from rhodecode.model.scm import UserGroupList
1314 from rhodecode.model.scm import UserGroupList
1314 if not perms:
1315 if not perms:
1315 perms = [
1316 perms = [
1316 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1317 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1317
1318
1318 def _cached_user_group_acl(user_id, perm_def, name_filter):
1319 def _cached_user_group_acl(user_id, perm_def, name_filter):
1319 qry = UserGroup.query()
1320 qry = UserGroup.query()
1320 if name_filter:
1321 if name_filter:
1321 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1322 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1322 qry = qry.filter(
1323 qry = qry.filter(
1323 UserGroup.users_group_name.ilike(ilike_expression))
1324 UserGroup.users_group_name.ilike(ilike_expression))
1324
1325
1325 return [x.users_group_id for x in
1326 return [x.users_group_id for x in
1326 UserGroupList(qry, perm_set=perm_def)]
1327 UserGroupList(qry, perm_set=perm_def)]
1327
1328
1328 return _cached_user_group_acl(self.user_id, perms, name_filter)
1329 return _cached_user_group_acl(self.user_id, perms, name_filter)
1329
1330
1330 @property
1331 @property
1331 def ip_allowed(self):
1332 def ip_allowed(self):
1332 """
1333 """
1333 Checks if ip_addr used in constructor is allowed from defined list of
1334 Checks if ip_addr used in constructor is allowed from defined list of
1334 allowed ip_addresses for user
1335 allowed ip_addresses for user
1335
1336
1336 :returns: boolean, True if ip is in allowed ip range
1337 :returns: boolean, True if ip is in allowed ip range
1337 """
1338 """
1338 # check IP
1339 # check IP
1339 inherit = self.inherit_default_permissions
1340 inherit = self.inherit_default_permissions
1340 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1341 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1341 inherit_from_default=inherit)
1342 inherit_from_default=inherit)
1342 @property
1343 @property
1343 def personal_repo_group(self):
1344 def personal_repo_group(self):
1344 return RepoGroup.get_user_personal_repo_group(self.user_id)
1345 return RepoGroup.get_user_personal_repo_group(self.user_id)
1345
1346
1346 @LazyProperty
1347 @LazyProperty
1347 def feed_token(self):
1348 def feed_token(self):
1348 return self.get_instance().feed_token
1349 return self.get_instance().feed_token
1349
1350
1350 @classmethod
1351 @classmethod
1351 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1352 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1352 allowed_ips = AuthUser.get_allowed_ips(
1353 allowed_ips = AuthUser.get_allowed_ips(
1353 user_id, cache=True, inherit_from_default=inherit_from_default)
1354 user_id, cache=True, inherit_from_default=inherit_from_default)
1354 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1355 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1355 log.debug('IP:%s for user %s is in range of %s' % (
1356 log.debug('IP:%s for user %s is in range of %s',
1356 ip_addr, user_id, allowed_ips))
1357 ip_addr, user_id, allowed_ips)
1357 return True
1358 return True
1358 else:
1359 else:
1359 log.info('Access for IP:%s forbidden for user %s, '
1360 log.info('Access for IP:%s forbidden for user %s, '
1360 'not in %s' % (ip_addr, user_id, allowed_ips))
1361 'not in %s', ip_addr, user_id, allowed_ips)
1361 return False
1362 return False
1362
1363
1363 def get_branch_permissions(self, repo_name, perms=None):
1364 def get_branch_permissions(self, repo_name, perms=None):
1364 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1365 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1365 branch_perms = perms.get('repository_branches', {})
1366 branch_perms = perms.get('repository_branches', {})
1366 if not branch_perms:
1367 if not branch_perms:
1367 return {}
1368 return {}
1368 repo_branch_perms = branch_perms.get(repo_name)
1369 repo_branch_perms = branch_perms.get(repo_name)
1369 return repo_branch_perms or {}
1370 return repo_branch_perms or {}
1370
1371
1371 def get_rule_and_branch_permission(self, repo_name, branch_name):
1372 def get_rule_and_branch_permission(self, repo_name, branch_name):
1372 """
1373 """
1373 Check if this AuthUser has defined any permissions for branches. If any of
1374 Check if this AuthUser has defined any permissions for branches. If any of
1374 the rules match in order, we return the matching permissions
1375 the rules match in order, we return the matching permissions
1375 """
1376 """
1376
1377
1377 rule = default_perm = ''
1378 rule = default_perm = ''
1378
1379
1379 repo_branch_perms = self.get_branch_permissions(repo_name=repo_name)
1380 repo_branch_perms = self.get_branch_permissions(repo_name=repo_name)
1380 if not repo_branch_perms:
1381 if not repo_branch_perms:
1381 return rule, default_perm
1382 return rule, default_perm
1382
1383
1383 # now calculate the permissions
1384 # now calculate the permissions
1384 for pattern, branch_perm in repo_branch_perms.items():
1385 for pattern, branch_perm in repo_branch_perms.items():
1385 if fnmatch.fnmatch(branch_name, pattern):
1386 if fnmatch.fnmatch(branch_name, pattern):
1386 rule = '`{}`=>{}'.format(pattern, branch_perm)
1387 rule = '`{}`=>{}'.format(pattern, branch_perm)
1387 return rule, branch_perm
1388 return rule, branch_perm
1388
1389
1389 return rule, default_perm
1390 return rule, default_perm
1390
1391
1391 def __repr__(self):
1392 def __repr__(self):
1392 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1393 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1393 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1394 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1394
1395
1395 def set_authenticated(self, authenticated=True):
1396 def set_authenticated(self, authenticated=True):
1396 if self.user_id != self.anonymous_user.user_id:
1397 if self.user_id != self.anonymous_user.user_id:
1397 self.is_authenticated = authenticated
1398 self.is_authenticated = authenticated
1398
1399
1399 def get_cookie_store(self):
1400 def get_cookie_store(self):
1400 return {
1401 return {
1401 'username': self.username,
1402 'username': self.username,
1402 'password': md5(self.password or ''),
1403 'password': md5(self.password or ''),
1403 'user_id': self.user_id,
1404 'user_id': self.user_id,
1404 'is_authenticated': self.is_authenticated
1405 'is_authenticated': self.is_authenticated
1405 }
1406 }
1406
1407
1407 @classmethod
1408 @classmethod
1408 def from_cookie_store(cls, cookie_store):
1409 def from_cookie_store(cls, cookie_store):
1409 """
1410 """
1410 Creates AuthUser from a cookie store
1411 Creates AuthUser from a cookie store
1411
1412
1412 :param cls:
1413 :param cls:
1413 :param cookie_store:
1414 :param cookie_store:
1414 """
1415 """
1415 user_id = cookie_store.get('user_id')
1416 user_id = cookie_store.get('user_id')
1416 username = cookie_store.get('username')
1417 username = cookie_store.get('username')
1417 api_key = cookie_store.get('api_key')
1418 api_key = cookie_store.get('api_key')
1418 return AuthUser(user_id, api_key, username)
1419 return AuthUser(user_id, api_key, username)
1419
1420
1420 @classmethod
1421 @classmethod
1421 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1422 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1422 _set = set()
1423 _set = set()
1423
1424
1424 if inherit_from_default:
1425 if inherit_from_default:
1425 def_user_id = User.get_default_user(cache=True).user_id
1426 def_user_id = User.get_default_user(cache=True).user_id
1426 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1427 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1427 if cache:
1428 if cache:
1428 default_ips = default_ips.options(
1429 default_ips = default_ips.options(
1429 FromCache("sql_cache_short", "get_user_ips_default"))
1430 FromCache("sql_cache_short", "get_user_ips_default"))
1430
1431
1431 # populate from default user
1432 # populate from default user
1432 for ip in default_ips:
1433 for ip in default_ips:
1433 try:
1434 try:
1434 _set.add(ip.ip_addr)
1435 _set.add(ip.ip_addr)
1435 except ObjectDeletedError:
1436 except ObjectDeletedError:
1436 # since we use heavy caching sometimes it happens that
1437 # since we use heavy caching sometimes it happens that
1437 # we get deleted objects here, we just skip them
1438 # we get deleted objects here, we just skip them
1438 pass
1439 pass
1439
1440
1440 # NOTE:(marcink) we don't want to load any rules for empty
1441 # NOTE:(marcink) we don't want to load any rules for empty
1441 # user_id which is the case of access of non logged users when anonymous
1442 # user_id which is the case of access of non logged users when anonymous
1442 # access is disabled
1443 # access is disabled
1443 user_ips = []
1444 user_ips = []
1444 if user_id:
1445 if user_id:
1445 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1446 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1446 if cache:
1447 if cache:
1447 user_ips = user_ips.options(
1448 user_ips = user_ips.options(
1448 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1449 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1449
1450
1450 for ip in user_ips:
1451 for ip in user_ips:
1451 try:
1452 try:
1452 _set.add(ip.ip_addr)
1453 _set.add(ip.ip_addr)
1453 except ObjectDeletedError:
1454 except ObjectDeletedError:
1454 # since we use heavy caching sometimes it happens that we get
1455 # since we use heavy caching sometimes it happens that we get
1455 # deleted objects here, we just skip them
1456 # deleted objects here, we just skip them
1456 pass
1457 pass
1457 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1458 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1458
1459
1459
1460
1460 def set_available_permissions(settings):
1461 def set_available_permissions(settings):
1461 """
1462 """
1462 This function will propagate pyramid settings with all available defined
1463 This function will propagate pyramid settings with all available defined
1463 permission given in db. We don't want to check each time from db for new
1464 permission given in db. We don't want to check each time from db for new
1464 permissions since adding a new permission also requires application restart
1465 permissions since adding a new permission also requires application restart
1465 ie. to decorate new views with the newly created permission
1466 ie. to decorate new views with the newly created permission
1466
1467
1467 :param settings: current pyramid registry.settings
1468 :param settings: current pyramid registry.settings
1468
1469
1469 """
1470 """
1470 log.debug('auth: getting information about all available permissions')
1471 log.debug('auth: getting information about all available permissions')
1471 try:
1472 try:
1472 sa = meta.Session
1473 sa = meta.Session
1473 all_perms = sa.query(Permission).all()
1474 all_perms = sa.query(Permission).all()
1474 settings.setdefault('available_permissions',
1475 settings.setdefault('available_permissions',
1475 [x.permission_name for x in all_perms])
1476 [x.permission_name for x in all_perms])
1476 log.debug('auth: set available permissions')
1477 log.debug('auth: set available permissions')
1477 except Exception:
1478 except Exception:
1478 log.exception('Failed to fetch permissions from the database.')
1479 log.exception('Failed to fetch permissions from the database.')
1479 raise
1480 raise
1480
1481
1481
1482
1482 def get_csrf_token(session, force_new=False, save_if_missing=True):
1483 def get_csrf_token(session, force_new=False, save_if_missing=True):
1483 """
1484 """
1484 Return the current authentication token, creating one if one doesn't
1485 Return the current authentication token, creating one if one doesn't
1485 already exist and the save_if_missing flag is present.
1486 already exist and the save_if_missing flag is present.
1486
1487
1487 :param session: pass in the pyramid session, else we use the global ones
1488 :param session: pass in the pyramid session, else we use the global ones
1488 :param force_new: force to re-generate the token and store it in session
1489 :param force_new: force to re-generate the token and store it in session
1489 :param save_if_missing: save the newly generated token if it's missing in
1490 :param save_if_missing: save the newly generated token if it's missing in
1490 session
1491 session
1491 """
1492 """
1492 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1493 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1493 # from pyramid.csrf import get_csrf_token
1494 # from pyramid.csrf import get_csrf_token
1494
1495
1495 if (csrf_token_key not in session and save_if_missing) or force_new:
1496 if (csrf_token_key not in session and save_if_missing) or force_new:
1496 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1497 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1497 session[csrf_token_key] = token
1498 session[csrf_token_key] = token
1498 if hasattr(session, 'save'):
1499 if hasattr(session, 'save'):
1499 session.save()
1500 session.save()
1500 return session.get(csrf_token_key)
1501 return session.get(csrf_token_key)
1501
1502
1502
1503
1503 def get_request(perm_class_instance):
1504 def get_request(perm_class_instance):
1504 from pyramid.threadlocal import get_current_request
1505 from pyramid.threadlocal import get_current_request
1505 pyramid_request = get_current_request()
1506 pyramid_request = get_current_request()
1506 return pyramid_request
1507 return pyramid_request
1507
1508
1508
1509
1509 # CHECK DECORATORS
1510 # CHECK DECORATORS
1510 class CSRFRequired(object):
1511 class CSRFRequired(object):
1511 """
1512 """
1512 Decorator for authenticating a form
1513 Decorator for authenticating a form
1513
1514
1514 This decorator uses an authorization token stored in the client's
1515 This decorator uses an authorization token stored in the client's
1515 session for prevention of certain Cross-site request forgery (CSRF)
1516 session for prevention of certain Cross-site request forgery (CSRF)
1516 attacks (See
1517 attacks (See
1517 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1518 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1518 information).
1519 information).
1519
1520
1520 For use with the ``webhelpers.secure_form`` helper functions.
1521 For use with the ``webhelpers.secure_form`` helper functions.
1521
1522
1522 """
1523 """
1523 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1524 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1524 except_methods=None):
1525 except_methods=None):
1525 self.token = token
1526 self.token = token
1526 self.header = header
1527 self.header = header
1527 self.except_methods = except_methods or []
1528 self.except_methods = except_methods or []
1528
1529
1529 def __call__(self, func):
1530 def __call__(self, func):
1530 return get_cython_compat_decorator(self.__wrapper, func)
1531 return get_cython_compat_decorator(self.__wrapper, func)
1531
1532
1532 def _get_csrf(self, _request):
1533 def _get_csrf(self, _request):
1533 return _request.POST.get(self.token, _request.headers.get(self.header))
1534 return _request.POST.get(self.token, _request.headers.get(self.header))
1534
1535
1535 def check_csrf(self, _request, cur_token):
1536 def check_csrf(self, _request, cur_token):
1536 supplied_token = self._get_csrf(_request)
1537 supplied_token = self._get_csrf(_request)
1537 return supplied_token and supplied_token == cur_token
1538 return supplied_token and supplied_token == cur_token
1538
1539
1539 def _get_request(self):
1540 def _get_request(self):
1540 return get_request(self)
1541 return get_request(self)
1541
1542
1542 def __wrapper(self, func, *fargs, **fkwargs):
1543 def __wrapper(self, func, *fargs, **fkwargs):
1543 request = self._get_request()
1544 request = self._get_request()
1544
1545
1545 if request.method in self.except_methods:
1546 if request.method in self.except_methods:
1546 return func(*fargs, **fkwargs)
1547 return func(*fargs, **fkwargs)
1547
1548
1548 cur_token = get_csrf_token(request.session, save_if_missing=False)
1549 cur_token = get_csrf_token(request.session, save_if_missing=False)
1549 if self.check_csrf(request, cur_token):
1550 if self.check_csrf(request, cur_token):
1550 if request.POST.get(self.token):
1551 if request.POST.get(self.token):
1551 del request.POST[self.token]
1552 del request.POST[self.token]
1552 return func(*fargs, **fkwargs)
1553 return func(*fargs, **fkwargs)
1553 else:
1554 else:
1554 reason = 'token-missing'
1555 reason = 'token-missing'
1555 supplied_token = self._get_csrf(request)
1556 supplied_token = self._get_csrf(request)
1556 if supplied_token and cur_token != supplied_token:
1557 if supplied_token and cur_token != supplied_token:
1557 reason = 'token-mismatch [%s:%s]' % (
1558 reason = 'token-mismatch [%s:%s]' % (
1558 cur_token or ''[:6], supplied_token or ''[:6])
1559 cur_token or ''[:6], supplied_token or ''[:6])
1559
1560
1560 csrf_message = \
1561 csrf_message = \
1561 ("Cross-site request forgery detected, request denied. See "
1562 ("Cross-site request forgery detected, request denied. See "
1562 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1563 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1563 "more information.")
1564 "more information.")
1564 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1565 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1565 'REMOTE_ADDR:%s, HEADERS:%s' % (
1566 'REMOTE_ADDR:%s, HEADERS:%s' % (
1566 request, reason, request.remote_addr, request.headers))
1567 request, reason, request.remote_addr, request.headers))
1567
1568
1568 raise HTTPForbidden(explanation=csrf_message)
1569 raise HTTPForbidden(explanation=csrf_message)
1569
1570
1570
1571
1571 class LoginRequired(object):
1572 class LoginRequired(object):
1572 """
1573 """
1573 Must be logged in to execute this function else
1574 Must be logged in to execute this function else
1574 redirect to login page
1575 redirect to login page
1575
1576
1576 :param api_access: if enabled this checks only for valid auth token
1577 :param api_access: if enabled this checks only for valid auth token
1577 and grants access based on valid token
1578 and grants access based on valid token
1578 """
1579 """
1579 def __init__(self, auth_token_access=None):
1580 def __init__(self, auth_token_access=None):
1580 self.auth_token_access = auth_token_access
1581 self.auth_token_access = auth_token_access
1581
1582
1582 def __call__(self, func):
1583 def __call__(self, func):
1583 return get_cython_compat_decorator(self.__wrapper, func)
1584 return get_cython_compat_decorator(self.__wrapper, func)
1584
1585
1585 def _get_request(self):
1586 def _get_request(self):
1586 return get_request(self)
1587 return get_request(self)
1587
1588
1588 def __wrapper(self, func, *fargs, **fkwargs):
1589 def __wrapper(self, func, *fargs, **fkwargs):
1589 from rhodecode.lib import helpers as h
1590 from rhodecode.lib import helpers as h
1590 cls = fargs[0]
1591 cls = fargs[0]
1591 user = cls._rhodecode_user
1592 user = cls._rhodecode_user
1592 request = self._get_request()
1593 request = self._get_request()
1593 _ = request.translate
1594 _ = request.translate
1594
1595
1595 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1596 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1596 log.debug('Starting login restriction checks for user: %s' % (user,))
1597 log.debug('Starting login restriction checks for user: %s', user)
1597 # check if our IP is allowed
1598 # check if our IP is allowed
1598 ip_access_valid = True
1599 ip_access_valid = True
1599 if not user.ip_allowed:
1600 if not user.ip_allowed:
1600 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1601 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1601 category='warning')
1602 category='warning')
1602 ip_access_valid = False
1603 ip_access_valid = False
1603
1604
1604 # check if we used an APIKEY and it's a valid one
1605 # check if we used an APIKEY and it's a valid one
1605 # defined white-list of controllers which API access will be enabled
1606 # defined white-list of controllers which API access will be enabled
1606 _auth_token = request.GET.get(
1607 _auth_token = request.GET.get(
1607 'auth_token', '') or request.GET.get('api_key', '')
1608 'auth_token', '') or request.GET.get('api_key', '')
1608 auth_token_access_valid = allowed_auth_token_access(
1609 auth_token_access_valid = allowed_auth_token_access(
1609 loc, auth_token=_auth_token)
1610 loc, auth_token=_auth_token)
1610
1611
1611 # explicit controller is enabled or API is in our whitelist
1612 # explicit controller is enabled or API is in our whitelist
1612 if self.auth_token_access or auth_token_access_valid:
1613 if self.auth_token_access or auth_token_access_valid:
1613 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1614 log.debug('Checking AUTH TOKEN access for %s', cls)
1614 db_user = user.get_instance()
1615 db_user = user.get_instance()
1615
1616
1616 if db_user:
1617 if db_user:
1617 if self.auth_token_access:
1618 if self.auth_token_access:
1618 roles = self.auth_token_access
1619 roles = self.auth_token_access
1619 else:
1620 else:
1620 roles = [UserApiKeys.ROLE_HTTP]
1621 roles = [UserApiKeys.ROLE_HTTP]
1621 token_match = db_user.authenticate_by_token(
1622 token_match = db_user.authenticate_by_token(
1622 _auth_token, roles=roles)
1623 _auth_token, roles=roles)
1623 else:
1624 else:
1624 log.debug('Unable to fetch db instance for auth user: %s', user)
1625 log.debug('Unable to fetch db instance for auth user: %s', user)
1625 token_match = False
1626 token_match = False
1626
1627
1627 if _auth_token and token_match:
1628 if _auth_token and token_match:
1628 auth_token_access_valid = True
1629 auth_token_access_valid = True
1629 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1630 log.debug('AUTH TOKEN ****%s is VALID', _auth_token[-4:])
1630 else:
1631 else:
1631 auth_token_access_valid = False
1632 auth_token_access_valid = False
1632 if not _auth_token:
1633 if not _auth_token:
1633 log.debug("AUTH TOKEN *NOT* present in request")
1634 log.debug("AUTH TOKEN *NOT* present in request")
1634 else:
1635 else:
1635 log.warning(
1636 log.warning("AUTH TOKEN ****%s *NOT* valid", _auth_token[-4:])
1636 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1637
1637
1638 log.debug('Checking if %s is authenticated @ %s', user.username, loc)
1638 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1639 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1639 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1640 else 'AUTH_TOKEN_AUTH'
1640 else 'AUTH_TOKEN_AUTH'
1641
1641
1642 if ip_access_valid and (
1642 if ip_access_valid and (
1643 user.is_authenticated or auth_token_access_valid):
1643 user.is_authenticated or auth_token_access_valid):
1644 log.info(
1644 log.info('user %s authenticating with:%s IS authenticated on func %s',
1645 'user %s authenticating with:%s IS authenticated on func %s'
1645 user, reason, loc)
1646 % (user, reason, loc))
1647
1646
1648 return func(*fargs, **fkwargs)
1647 return func(*fargs, **fkwargs)
1649 else:
1648 else:
1650 log.warning(
1649 log.warning(
1651 'user %s authenticating with:%s NOT authenticated on '
1650 'user %s authenticating with:%s NOT authenticated on '
1652 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1651 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s',
1653 % (user, reason, loc, ip_access_valid,
1652 user, reason, loc, ip_access_valid, auth_token_access_valid)
1654 auth_token_access_valid))
1655 # we preserve the get PARAM
1653 # we preserve the get PARAM
1656 came_from = get_came_from(request)
1654 came_from = get_came_from(request)
1657
1655
1658 log.debug('redirecting to login page with %s' % (came_from,))
1656 log.debug('redirecting to login page with %s', came_from)
1659 raise HTTPFound(
1657 raise HTTPFound(
1660 h.route_path('login', _query={'came_from': came_from}))
1658 h.route_path('login', _query={'came_from': came_from}))
1661
1659
1662
1660
1663 class NotAnonymous(object):
1661 class NotAnonymous(object):
1664 """
1662 """
1665 Must be logged in to execute this function else
1663 Must be logged in to execute this function else
1666 redirect to login page
1664 redirect to login page
1667 """
1665 """
1668
1666
1669 def __call__(self, func):
1667 def __call__(self, func):
1670 return get_cython_compat_decorator(self.__wrapper, func)
1668 return get_cython_compat_decorator(self.__wrapper, func)
1671
1669
1672 def _get_request(self):
1670 def _get_request(self):
1673 return get_request(self)
1671 return get_request(self)
1674
1672
1675 def __wrapper(self, func, *fargs, **fkwargs):
1673 def __wrapper(self, func, *fargs, **fkwargs):
1676 import rhodecode.lib.helpers as h
1674 import rhodecode.lib.helpers as h
1677 cls = fargs[0]
1675 cls = fargs[0]
1678 self.user = cls._rhodecode_user
1676 self.user = cls._rhodecode_user
1679 request = self._get_request()
1677 request = self._get_request()
1680 _ = request.translate
1678 _ = request.translate
1681 log.debug('Checking if user is not anonymous @%s' % cls)
1679 log.debug('Checking if user is not anonymous @%s', cls)
1682
1680
1683 anonymous = self.user.username == User.DEFAULT_USER
1681 anonymous = self.user.username == User.DEFAULT_USER
1684
1682
1685 if anonymous:
1683 if anonymous:
1686 came_from = get_came_from(request)
1684 came_from = get_came_from(request)
1687 h.flash(_('You need to be a registered user to '
1685 h.flash(_('You need to be a registered user to '
1688 'perform this action'),
1686 'perform this action'),
1689 category='warning')
1687 category='warning')
1690 raise HTTPFound(
1688 raise HTTPFound(
1691 h.route_path('login', _query={'came_from': came_from}))
1689 h.route_path('login', _query={'came_from': came_from}))
1692 else:
1690 else:
1693 return func(*fargs, **fkwargs)
1691 return func(*fargs, **fkwargs)
1694
1692
1695
1693
1696 class PermsDecorator(object):
1694 class PermsDecorator(object):
1697 """
1695 """
1698 Base class for controller decorators, we extract the current user from
1696 Base class for controller decorators, we extract the current user from
1699 the class itself, which has it stored in base controllers
1697 the class itself, which has it stored in base controllers
1700 """
1698 """
1701
1699
1702 def __init__(self, *required_perms):
1700 def __init__(self, *required_perms):
1703 self.required_perms = set(required_perms)
1701 self.required_perms = set(required_perms)
1704
1702
1705 def __call__(self, func):
1703 def __call__(self, func):
1706 return get_cython_compat_decorator(self.__wrapper, func)
1704 return get_cython_compat_decorator(self.__wrapper, func)
1707
1705
1708 def _get_request(self):
1706 def _get_request(self):
1709 return get_request(self)
1707 return get_request(self)
1710
1708
1711 def __wrapper(self, func, *fargs, **fkwargs):
1709 def __wrapper(self, func, *fargs, **fkwargs):
1712 import rhodecode.lib.helpers as h
1710 import rhodecode.lib.helpers as h
1713 cls = fargs[0]
1711 cls = fargs[0]
1714 _user = cls._rhodecode_user
1712 _user = cls._rhodecode_user
1715 request = self._get_request()
1713 request = self._get_request()
1716 _ = request.translate
1714 _ = request.translate
1717
1715
1718 log.debug('checking %s permissions %s for %s %s',
1716 log.debug('checking %s permissions %s for %s %s',
1719 self.__class__.__name__, self.required_perms, cls, _user)
1717 self.__class__.__name__, self.required_perms, cls, _user)
1720
1718
1721 if self.check_permissions(_user):
1719 if self.check_permissions(_user):
1722 log.debug('Permission granted for %s %s', cls, _user)
1720 log.debug('Permission granted for %s %s', cls, _user)
1723 return func(*fargs, **fkwargs)
1721 return func(*fargs, **fkwargs)
1724
1722
1725 else:
1723 else:
1726 log.debug('Permission denied for %s %s', cls, _user)
1724 log.debug('Permission denied for %s %s', cls, _user)
1727 anonymous = _user.username == User.DEFAULT_USER
1725 anonymous = _user.username == User.DEFAULT_USER
1728
1726
1729 if anonymous:
1727 if anonymous:
1730 came_from = get_came_from(self._get_request())
1728 came_from = get_came_from(self._get_request())
1731 h.flash(_('You need to be signed in to view this page'),
1729 h.flash(_('You need to be signed in to view this page'),
1732 category='warning')
1730 category='warning')
1733 raise HTTPFound(
1731 raise HTTPFound(
1734 h.route_path('login', _query={'came_from': came_from}))
1732 h.route_path('login', _query={'came_from': came_from}))
1735
1733
1736 else:
1734 else:
1737 # redirect with 404 to prevent resource discovery
1735 # redirect with 404 to prevent resource discovery
1738 raise HTTPNotFound()
1736 raise HTTPNotFound()
1739
1737
1740 def check_permissions(self, user):
1738 def check_permissions(self, user):
1741 """Dummy function for overriding"""
1739 """Dummy function for overriding"""
1742 raise NotImplementedError(
1740 raise NotImplementedError(
1743 'You have to write this function in child class')
1741 'You have to write this function in child class')
1744
1742
1745
1743
1746 class HasPermissionAllDecorator(PermsDecorator):
1744 class HasPermissionAllDecorator(PermsDecorator):
1747 """
1745 """
1748 Checks for access permission for all given predicates. All of them
1746 Checks for access permission for all given predicates. All of them
1749 have to be meet in order to fulfill the request
1747 have to be meet in order to fulfill the request
1750 """
1748 """
1751
1749
1752 def check_permissions(self, user):
1750 def check_permissions(self, user):
1753 perms = user.permissions_with_scope({})
1751 perms = user.permissions_with_scope({})
1754 if self.required_perms.issubset(perms['global']):
1752 if self.required_perms.issubset(perms['global']):
1755 return True
1753 return True
1756 return False
1754 return False
1757
1755
1758
1756
1759 class HasPermissionAnyDecorator(PermsDecorator):
1757 class HasPermissionAnyDecorator(PermsDecorator):
1760 """
1758 """
1761 Checks for access permission for any of given predicates. In order to
1759 Checks for access permission for any of given predicates. In order to
1762 fulfill the request any of predicates must be meet
1760 fulfill the request any of predicates must be meet
1763 """
1761 """
1764
1762
1765 def check_permissions(self, user):
1763 def check_permissions(self, user):
1766 perms = user.permissions_with_scope({})
1764 perms = user.permissions_with_scope({})
1767 if self.required_perms.intersection(perms['global']):
1765 if self.required_perms.intersection(perms['global']):
1768 return True
1766 return True
1769 return False
1767 return False
1770
1768
1771
1769
1772 class HasRepoPermissionAllDecorator(PermsDecorator):
1770 class HasRepoPermissionAllDecorator(PermsDecorator):
1773 """
1771 """
1774 Checks for access permission for all given predicates for specific
1772 Checks for access permission for all given predicates for specific
1775 repository. All of them have to be meet in order to fulfill the request
1773 repository. All of them have to be meet in order to fulfill the request
1776 """
1774 """
1777 def _get_repo_name(self):
1775 def _get_repo_name(self):
1778 _request = self._get_request()
1776 _request = self._get_request()
1779 return get_repo_slug(_request)
1777 return get_repo_slug(_request)
1780
1778
1781 def check_permissions(self, user):
1779 def check_permissions(self, user):
1782 perms = user.permissions
1780 perms = user.permissions
1783 repo_name = self._get_repo_name()
1781 repo_name = self._get_repo_name()
1784
1782
1785 try:
1783 try:
1786 user_perms = {perms['repositories'][repo_name]}
1784 user_perms = {perms['repositories'][repo_name]}
1787 except KeyError:
1785 except KeyError:
1788 log.debug('cannot locate repo with name: `%s` in permissions defs',
1786 log.debug('cannot locate repo with name: `%s` in permissions defs',
1789 repo_name)
1787 repo_name)
1790 return False
1788 return False
1791
1789
1792 log.debug('checking `%s` permissions for repo `%s`',
1790 log.debug('checking `%s` permissions for repo `%s`',
1793 user_perms, repo_name)
1791 user_perms, repo_name)
1794 if self.required_perms.issubset(user_perms):
1792 if self.required_perms.issubset(user_perms):
1795 return True
1793 return True
1796 return False
1794 return False
1797
1795
1798
1796
1799 class HasRepoPermissionAnyDecorator(PermsDecorator):
1797 class HasRepoPermissionAnyDecorator(PermsDecorator):
1800 """
1798 """
1801 Checks for access permission for any of given predicates for specific
1799 Checks for access permission for any of given predicates for specific
1802 repository. In order to fulfill the request any of predicates must be meet
1800 repository. In order to fulfill the request any of predicates must be meet
1803 """
1801 """
1804 def _get_repo_name(self):
1802 def _get_repo_name(self):
1805 _request = self._get_request()
1803 _request = self._get_request()
1806 return get_repo_slug(_request)
1804 return get_repo_slug(_request)
1807
1805
1808 def check_permissions(self, user):
1806 def check_permissions(self, user):
1809 perms = user.permissions
1807 perms = user.permissions
1810 repo_name = self._get_repo_name()
1808 repo_name = self._get_repo_name()
1811
1809
1812 try:
1810 try:
1813 user_perms = {perms['repositories'][repo_name]}
1811 user_perms = {perms['repositories'][repo_name]}
1814 except KeyError:
1812 except KeyError:
1815 log.debug(
1813 log.debug(
1816 'cannot locate repo with name: `%s` in permissions defs',
1814 'cannot locate repo with name: `%s` in permissions defs',
1817 repo_name)
1815 repo_name)
1818 return False
1816 return False
1819
1817
1820 log.debug('checking `%s` permissions for repo `%s`',
1818 log.debug('checking `%s` permissions for repo `%s`',
1821 user_perms, repo_name)
1819 user_perms, repo_name)
1822 if self.required_perms.intersection(user_perms):
1820 if self.required_perms.intersection(user_perms):
1823 return True
1821 return True
1824 return False
1822 return False
1825
1823
1826
1824
1827 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1825 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1828 """
1826 """
1829 Checks for access permission for all given predicates for specific
1827 Checks for access permission for all given predicates for specific
1830 repository group. All of them have to be meet in order to
1828 repository group. All of them have to be meet in order to
1831 fulfill the request
1829 fulfill the request
1832 """
1830 """
1833 def _get_repo_group_name(self):
1831 def _get_repo_group_name(self):
1834 _request = self._get_request()
1832 _request = self._get_request()
1835 return get_repo_group_slug(_request)
1833 return get_repo_group_slug(_request)
1836
1834
1837 def check_permissions(self, user):
1835 def check_permissions(self, user):
1838 perms = user.permissions
1836 perms = user.permissions
1839 group_name = self._get_repo_group_name()
1837 group_name = self._get_repo_group_name()
1840 try:
1838 try:
1841 user_perms = {perms['repositories_groups'][group_name]}
1839 user_perms = {perms['repositories_groups'][group_name]}
1842 except KeyError:
1840 except KeyError:
1843 log.debug(
1841 log.debug(
1844 'cannot locate repo group with name: `%s` in permissions defs',
1842 'cannot locate repo group with name: `%s` in permissions defs',
1845 group_name)
1843 group_name)
1846 return False
1844 return False
1847
1845
1848 log.debug('checking `%s` permissions for repo group `%s`',
1846 log.debug('checking `%s` permissions for repo group `%s`',
1849 user_perms, group_name)
1847 user_perms, group_name)
1850 if self.required_perms.issubset(user_perms):
1848 if self.required_perms.issubset(user_perms):
1851 return True
1849 return True
1852 return False
1850 return False
1853
1851
1854
1852
1855 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1853 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1856 """
1854 """
1857 Checks for access permission for any of given predicates for specific
1855 Checks for access permission for any of given predicates for specific
1858 repository group. In order to fulfill the request any
1856 repository group. In order to fulfill the request any
1859 of predicates must be met
1857 of predicates must be met
1860 """
1858 """
1861 def _get_repo_group_name(self):
1859 def _get_repo_group_name(self):
1862 _request = self._get_request()
1860 _request = self._get_request()
1863 return get_repo_group_slug(_request)
1861 return get_repo_group_slug(_request)
1864
1862
1865 def check_permissions(self, user):
1863 def check_permissions(self, user):
1866 perms = user.permissions
1864 perms = user.permissions
1867 group_name = self._get_repo_group_name()
1865 group_name = self._get_repo_group_name()
1868
1866
1869 try:
1867 try:
1870 user_perms = {perms['repositories_groups'][group_name]}
1868 user_perms = {perms['repositories_groups'][group_name]}
1871 except KeyError:
1869 except KeyError:
1872 log.debug(
1870 log.debug(
1873 'cannot locate repo group with name: `%s` in permissions defs',
1871 'cannot locate repo group with name: `%s` in permissions defs',
1874 group_name)
1872 group_name)
1875 return False
1873 return False
1876
1874
1877 log.debug('checking `%s` permissions for repo group `%s`',
1875 log.debug('checking `%s` permissions for repo group `%s`',
1878 user_perms, group_name)
1876 user_perms, group_name)
1879 if self.required_perms.intersection(user_perms):
1877 if self.required_perms.intersection(user_perms):
1880 return True
1878 return True
1881 return False
1879 return False
1882
1880
1883
1881
1884 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1882 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1885 """
1883 """
1886 Checks for access permission for all given predicates for specific
1884 Checks for access permission for all given predicates for specific
1887 user group. All of them have to be meet in order to fulfill the request
1885 user group. All of them have to be meet in order to fulfill the request
1888 """
1886 """
1889 def _get_user_group_name(self):
1887 def _get_user_group_name(self):
1890 _request = self._get_request()
1888 _request = self._get_request()
1891 return get_user_group_slug(_request)
1889 return get_user_group_slug(_request)
1892
1890
1893 def check_permissions(self, user):
1891 def check_permissions(self, user):
1894 perms = user.permissions
1892 perms = user.permissions
1895 group_name = self._get_user_group_name()
1893 group_name = self._get_user_group_name()
1896 try:
1894 try:
1897 user_perms = {perms['user_groups'][group_name]}
1895 user_perms = {perms['user_groups'][group_name]}
1898 except KeyError:
1896 except KeyError:
1899 return False
1897 return False
1900
1898
1901 if self.required_perms.issubset(user_perms):
1899 if self.required_perms.issubset(user_perms):
1902 return True
1900 return True
1903 return False
1901 return False
1904
1902
1905
1903
1906 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1904 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1907 """
1905 """
1908 Checks for access permission for any of given predicates for specific
1906 Checks for access permission for any of given predicates for specific
1909 user group. In order to fulfill the request any of predicates must be meet
1907 user group. In order to fulfill the request any of predicates must be meet
1910 """
1908 """
1911 def _get_user_group_name(self):
1909 def _get_user_group_name(self):
1912 _request = self._get_request()
1910 _request = self._get_request()
1913 return get_user_group_slug(_request)
1911 return get_user_group_slug(_request)
1914
1912
1915 def check_permissions(self, user):
1913 def check_permissions(self, user):
1916 perms = user.permissions
1914 perms = user.permissions
1917 group_name = self._get_user_group_name()
1915 group_name = self._get_user_group_name()
1918 try:
1916 try:
1919 user_perms = {perms['user_groups'][group_name]}
1917 user_perms = {perms['user_groups'][group_name]}
1920 except KeyError:
1918 except KeyError:
1921 return False
1919 return False
1922
1920
1923 if self.required_perms.intersection(user_perms):
1921 if self.required_perms.intersection(user_perms):
1924 return True
1922 return True
1925 return False
1923 return False
1926
1924
1927
1925
1928 # CHECK FUNCTIONS
1926 # CHECK FUNCTIONS
1929 class PermsFunction(object):
1927 class PermsFunction(object):
1930 """Base function for other check functions"""
1928 """Base function for other check functions"""
1931
1929
1932 def __init__(self, *perms):
1930 def __init__(self, *perms):
1933 self.required_perms = set(perms)
1931 self.required_perms = set(perms)
1934 self.repo_name = None
1932 self.repo_name = None
1935 self.repo_group_name = None
1933 self.repo_group_name = None
1936 self.user_group_name = None
1934 self.user_group_name = None
1937
1935
1938 def __bool__(self):
1936 def __bool__(self):
1939 frame = inspect.currentframe()
1937 frame = inspect.currentframe()
1940 stack_trace = traceback.format_stack(frame)
1938 stack_trace = traceback.format_stack(frame)
1941 log.error('Checking bool value on a class instance of perm '
1939 log.error('Checking bool value on a class instance of perm '
1942 'function is not allowed: %s' % ''.join(stack_trace))
1940 'function is not allowed: %s', ''.join(stack_trace))
1943 # rather than throwing errors, here we always return False so if by
1941 # rather than throwing errors, here we always return False so if by
1944 # accident someone checks truth for just an instance it will always end
1942 # accident someone checks truth for just an instance it will always end
1945 # up in returning False
1943 # up in returning False
1946 return False
1944 return False
1947 __nonzero__ = __bool__
1945 __nonzero__ = __bool__
1948
1946
1949 def __call__(self, check_location='', user=None):
1947 def __call__(self, check_location='', user=None):
1950 if not user:
1948 if not user:
1951 log.debug('Using user attribute from global request')
1949 log.debug('Using user attribute from global request')
1952 request = self._get_request()
1950 request = self._get_request()
1953 user = request.user
1951 user = request.user
1954
1952
1955 # init auth user if not already given
1953 # init auth user if not already given
1956 if not isinstance(user, AuthUser):
1954 if not isinstance(user, AuthUser):
1957 log.debug('Wrapping user %s into AuthUser', user)
1955 log.debug('Wrapping user %s into AuthUser', user)
1958 user = AuthUser(user.user_id)
1956 user = AuthUser(user.user_id)
1959
1957
1960 cls_name = self.__class__.__name__
1958 cls_name = self.__class__.__name__
1961 check_scope = self._get_check_scope(cls_name)
1959 check_scope = self._get_check_scope(cls_name)
1962 check_location = check_location or 'unspecified location'
1960 check_location = check_location or 'unspecified location'
1963
1961
1964 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1962 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1965 self.required_perms, user, check_scope, check_location)
1963 self.required_perms, user, check_scope, check_location)
1966 if not user:
1964 if not user:
1967 log.warning('Empty user given for permission check')
1965 log.warning('Empty user given for permission check')
1968 return False
1966 return False
1969
1967
1970 if self.check_permissions(user):
1968 if self.check_permissions(user):
1971 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1969 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1972 check_scope, user, check_location)
1970 check_scope, user, check_location)
1973 return True
1971 return True
1974
1972
1975 else:
1973 else:
1976 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1974 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1977 check_scope, user, check_location)
1975 check_scope, user, check_location)
1978 return False
1976 return False
1979
1977
1980 def _get_request(self):
1978 def _get_request(self):
1981 return get_request(self)
1979 return get_request(self)
1982
1980
1983 def _get_check_scope(self, cls_name):
1981 def _get_check_scope(self, cls_name):
1984 return {
1982 return {
1985 'HasPermissionAll': 'GLOBAL',
1983 'HasPermissionAll': 'GLOBAL',
1986 'HasPermissionAny': 'GLOBAL',
1984 'HasPermissionAny': 'GLOBAL',
1987 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1985 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1988 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1986 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1989 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1987 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1990 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1988 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1991 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1989 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1992 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1990 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1993 }.get(cls_name, '?:%s' % cls_name)
1991 }.get(cls_name, '?:%s' % cls_name)
1994
1992
1995 def check_permissions(self, user):
1993 def check_permissions(self, user):
1996 """Dummy function for overriding"""
1994 """Dummy function for overriding"""
1997 raise Exception('You have to write this function in child class')
1995 raise Exception('You have to write this function in child class')
1998
1996
1999
1997
2000 class HasPermissionAll(PermsFunction):
1998 class HasPermissionAll(PermsFunction):
2001 def check_permissions(self, user):
1999 def check_permissions(self, user):
2002 perms = user.permissions_with_scope({})
2000 perms = user.permissions_with_scope({})
2003 if self.required_perms.issubset(perms.get('global')):
2001 if self.required_perms.issubset(perms.get('global')):
2004 return True
2002 return True
2005 return False
2003 return False
2006
2004
2007
2005
2008 class HasPermissionAny(PermsFunction):
2006 class HasPermissionAny(PermsFunction):
2009 def check_permissions(self, user):
2007 def check_permissions(self, user):
2010 perms = user.permissions_with_scope({})
2008 perms = user.permissions_with_scope({})
2011 if self.required_perms.intersection(perms.get('global')):
2009 if self.required_perms.intersection(perms.get('global')):
2012 return True
2010 return True
2013 return False
2011 return False
2014
2012
2015
2013
2016 class HasRepoPermissionAll(PermsFunction):
2014 class HasRepoPermissionAll(PermsFunction):
2017 def __call__(self, repo_name=None, check_location='', user=None):
2015 def __call__(self, repo_name=None, check_location='', user=None):
2018 self.repo_name = repo_name
2016 self.repo_name = repo_name
2019 return super(HasRepoPermissionAll, self).__call__(check_location, user)
2017 return super(HasRepoPermissionAll, self).__call__(check_location, user)
2020
2018
2021 def _get_repo_name(self):
2019 def _get_repo_name(self):
2022 if not self.repo_name:
2020 if not self.repo_name:
2023 _request = self._get_request()
2021 _request = self._get_request()
2024 self.repo_name = get_repo_slug(_request)
2022 self.repo_name = get_repo_slug(_request)
2025 return self.repo_name
2023 return self.repo_name
2026
2024
2027 def check_permissions(self, user):
2025 def check_permissions(self, user):
2028 self.repo_name = self._get_repo_name()
2026 self.repo_name = self._get_repo_name()
2029 perms = user.permissions
2027 perms = user.permissions
2030 try:
2028 try:
2031 user_perms = {perms['repositories'][self.repo_name]}
2029 user_perms = {perms['repositories'][self.repo_name]}
2032 except KeyError:
2030 except KeyError:
2033 return False
2031 return False
2034 if self.required_perms.issubset(user_perms):
2032 if self.required_perms.issubset(user_perms):
2035 return True
2033 return True
2036 return False
2034 return False
2037
2035
2038
2036
2039 class HasRepoPermissionAny(PermsFunction):
2037 class HasRepoPermissionAny(PermsFunction):
2040 def __call__(self, repo_name=None, check_location='', user=None):
2038 def __call__(self, repo_name=None, check_location='', user=None):
2041 self.repo_name = repo_name
2039 self.repo_name = repo_name
2042 return super(HasRepoPermissionAny, self).__call__(check_location, user)
2040 return super(HasRepoPermissionAny, self).__call__(check_location, user)
2043
2041
2044 def _get_repo_name(self):
2042 def _get_repo_name(self):
2045 if not self.repo_name:
2043 if not self.repo_name:
2046 _request = self._get_request()
2044 _request = self._get_request()
2047 self.repo_name = get_repo_slug(_request)
2045 self.repo_name = get_repo_slug(_request)
2048 return self.repo_name
2046 return self.repo_name
2049
2047
2050 def check_permissions(self, user):
2048 def check_permissions(self, user):
2051 self.repo_name = self._get_repo_name()
2049 self.repo_name = self._get_repo_name()
2052 perms = user.permissions
2050 perms = user.permissions
2053 try:
2051 try:
2054 user_perms = {perms['repositories'][self.repo_name]}
2052 user_perms = {perms['repositories'][self.repo_name]}
2055 except KeyError:
2053 except KeyError:
2056 return False
2054 return False
2057 if self.required_perms.intersection(user_perms):
2055 if self.required_perms.intersection(user_perms):
2058 return True
2056 return True
2059 return False
2057 return False
2060
2058
2061
2059
2062 class HasRepoGroupPermissionAny(PermsFunction):
2060 class HasRepoGroupPermissionAny(PermsFunction):
2063 def __call__(self, group_name=None, check_location='', user=None):
2061 def __call__(self, group_name=None, check_location='', user=None):
2064 self.repo_group_name = group_name
2062 self.repo_group_name = group_name
2065 return super(HasRepoGroupPermissionAny, self).__call__(
2063 return super(HasRepoGroupPermissionAny, self).__call__(
2066 check_location, user)
2064 check_location, user)
2067
2065
2068 def check_permissions(self, user):
2066 def check_permissions(self, user):
2069 perms = user.permissions
2067 perms = user.permissions
2070 try:
2068 try:
2071 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2069 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2072 except KeyError:
2070 except KeyError:
2073 return False
2071 return False
2074 if self.required_perms.intersection(user_perms):
2072 if self.required_perms.intersection(user_perms):
2075 return True
2073 return True
2076 return False
2074 return False
2077
2075
2078
2076
2079 class HasRepoGroupPermissionAll(PermsFunction):
2077 class HasRepoGroupPermissionAll(PermsFunction):
2080 def __call__(self, group_name=None, check_location='', user=None):
2078 def __call__(self, group_name=None, check_location='', user=None):
2081 self.repo_group_name = group_name
2079 self.repo_group_name = group_name
2082 return super(HasRepoGroupPermissionAll, self).__call__(
2080 return super(HasRepoGroupPermissionAll, self).__call__(
2083 check_location, user)
2081 check_location, user)
2084
2082
2085 def check_permissions(self, user):
2083 def check_permissions(self, user):
2086 perms = user.permissions
2084 perms = user.permissions
2087 try:
2085 try:
2088 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2086 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2089 except KeyError:
2087 except KeyError:
2090 return False
2088 return False
2091 if self.required_perms.issubset(user_perms):
2089 if self.required_perms.issubset(user_perms):
2092 return True
2090 return True
2093 return False
2091 return False
2094
2092
2095
2093
2096 class HasUserGroupPermissionAny(PermsFunction):
2094 class HasUserGroupPermissionAny(PermsFunction):
2097 def __call__(self, user_group_name=None, check_location='', user=None):
2095 def __call__(self, user_group_name=None, check_location='', user=None):
2098 self.user_group_name = user_group_name
2096 self.user_group_name = user_group_name
2099 return super(HasUserGroupPermissionAny, self).__call__(
2097 return super(HasUserGroupPermissionAny, self).__call__(
2100 check_location, user)
2098 check_location, user)
2101
2099
2102 def check_permissions(self, user):
2100 def check_permissions(self, user):
2103 perms = user.permissions
2101 perms = user.permissions
2104 try:
2102 try:
2105 user_perms = {perms['user_groups'][self.user_group_name]}
2103 user_perms = {perms['user_groups'][self.user_group_name]}
2106 except KeyError:
2104 except KeyError:
2107 return False
2105 return False
2108 if self.required_perms.intersection(user_perms):
2106 if self.required_perms.intersection(user_perms):
2109 return True
2107 return True
2110 return False
2108 return False
2111
2109
2112
2110
2113 class HasUserGroupPermissionAll(PermsFunction):
2111 class HasUserGroupPermissionAll(PermsFunction):
2114 def __call__(self, user_group_name=None, check_location='', user=None):
2112 def __call__(self, user_group_name=None, check_location='', user=None):
2115 self.user_group_name = user_group_name
2113 self.user_group_name = user_group_name
2116 return super(HasUserGroupPermissionAll, self).__call__(
2114 return super(HasUserGroupPermissionAll, self).__call__(
2117 check_location, user)
2115 check_location, user)
2118
2116
2119 def check_permissions(self, user):
2117 def check_permissions(self, user):
2120 perms = user.permissions
2118 perms = user.permissions
2121 try:
2119 try:
2122 user_perms = {perms['user_groups'][self.user_group_name]}
2120 user_perms = {perms['user_groups'][self.user_group_name]}
2123 except KeyError:
2121 except KeyError:
2124 return False
2122 return False
2125 if self.required_perms.issubset(user_perms):
2123 if self.required_perms.issubset(user_perms):
2126 return True
2124 return True
2127 return False
2125 return False
2128
2126
2129
2127
2130 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2128 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2131 class HasPermissionAnyMiddleware(object):
2129 class HasPermissionAnyMiddleware(object):
2132 def __init__(self, *perms):
2130 def __init__(self, *perms):
2133 self.required_perms = set(perms)
2131 self.required_perms = set(perms)
2134
2132
2135 def __call__(self, auth_user, repo_name):
2133 def __call__(self, auth_user, repo_name):
2136 # repo_name MUST be unicode, since we handle keys in permission
2134 # repo_name MUST be unicode, since we handle keys in permission
2137 # dict by unicode
2135 # dict by unicode
2138 repo_name = safe_unicode(repo_name)
2136 repo_name = safe_unicode(repo_name)
2139 log.debug(
2137 log.debug(
2140 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2138 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2141 self.required_perms, auth_user, repo_name)
2139 self.required_perms, auth_user, repo_name)
2142
2140
2143 if self.check_permissions(auth_user, repo_name):
2141 if self.check_permissions(auth_user, repo_name):
2144 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2142 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2145 repo_name, auth_user, 'PermissionMiddleware')
2143 repo_name, auth_user, 'PermissionMiddleware')
2146 return True
2144 return True
2147
2145
2148 else:
2146 else:
2149 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2147 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2150 repo_name, auth_user, 'PermissionMiddleware')
2148 repo_name, auth_user, 'PermissionMiddleware')
2151 return False
2149 return False
2152
2150
2153 def check_permissions(self, user, repo_name):
2151 def check_permissions(self, user, repo_name):
2154 perms = user.permissions_with_scope({'repo_name': repo_name})
2152 perms = user.permissions_with_scope({'repo_name': repo_name})
2155
2153
2156 try:
2154 try:
2157 user_perms = {perms['repositories'][repo_name]}
2155 user_perms = {perms['repositories'][repo_name]}
2158 except Exception:
2156 except Exception:
2159 log.exception('Error while accessing user permissions')
2157 log.exception('Error while accessing user permissions')
2160 return False
2158 return False
2161
2159
2162 if self.required_perms.intersection(user_perms):
2160 if self.required_perms.intersection(user_perms):
2163 return True
2161 return True
2164 return False
2162 return False
2165
2163
2166
2164
2167 # SPECIAL VERSION TO HANDLE API AUTH
2165 # SPECIAL VERSION TO HANDLE API AUTH
2168 class _BaseApiPerm(object):
2166 class _BaseApiPerm(object):
2169 def __init__(self, *perms):
2167 def __init__(self, *perms):
2170 self.required_perms = set(perms)
2168 self.required_perms = set(perms)
2171
2169
2172 def __call__(self, check_location=None, user=None, repo_name=None,
2170 def __call__(self, check_location=None, user=None, repo_name=None,
2173 group_name=None, user_group_name=None):
2171 group_name=None, user_group_name=None):
2174 cls_name = self.__class__.__name__
2172 cls_name = self.__class__.__name__
2175 check_scope = 'global:%s' % (self.required_perms,)
2173 check_scope = 'global:%s' % (self.required_perms,)
2176 if repo_name:
2174 if repo_name:
2177 check_scope += ', repo_name:%s' % (repo_name,)
2175 check_scope += ', repo_name:%s' % (repo_name,)
2178
2176
2179 if group_name:
2177 if group_name:
2180 check_scope += ', repo_group_name:%s' % (group_name,)
2178 check_scope += ', repo_group_name:%s' % (group_name,)
2181
2179
2182 if user_group_name:
2180 if user_group_name:
2183 check_scope += ', user_group_name:%s' % (user_group_name,)
2181 check_scope += ', user_group_name:%s' % (user_group_name,)
2184
2182
2185 log.debug(
2183 log.debug('checking cls:%s %s %s @ %s',
2186 'checking cls:%s %s %s @ %s'
2184 cls_name, self.required_perms, check_scope, check_location)
2187 % (cls_name, self.required_perms, check_scope, check_location))
2188 if not user:
2185 if not user:
2189 log.debug('Empty User passed into arguments')
2186 log.debug('Empty User passed into arguments')
2190 return False
2187 return False
2191
2188
2192 # process user
2189 # process user
2193 if not isinstance(user, AuthUser):
2190 if not isinstance(user, AuthUser):
2194 user = AuthUser(user.user_id)
2191 user = AuthUser(user.user_id)
2195 if not check_location:
2192 if not check_location:
2196 check_location = 'unspecified'
2193 check_location = 'unspecified'
2197 if self.check_permissions(user.permissions, repo_name, group_name,
2194 if self.check_permissions(user.permissions, repo_name, group_name,
2198 user_group_name):
2195 user_group_name):
2199 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2196 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2200 check_scope, user, check_location)
2197 check_scope, user, check_location)
2201 return True
2198 return True
2202
2199
2203 else:
2200 else:
2204 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2201 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2205 check_scope, user, check_location)
2202 check_scope, user, check_location)
2206 return False
2203 return False
2207
2204
2208 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2205 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2209 user_group_name=None):
2206 user_group_name=None):
2210 """
2207 """
2211 implement in child class should return True if permissions are ok,
2208 implement in child class should return True if permissions are ok,
2212 False otherwise
2209 False otherwise
2213
2210
2214 :param perm_defs: dict with permission definitions
2211 :param perm_defs: dict with permission definitions
2215 :param repo_name: repo name
2212 :param repo_name: repo name
2216 """
2213 """
2217 raise NotImplementedError()
2214 raise NotImplementedError()
2218
2215
2219
2216
2220 class HasPermissionAllApi(_BaseApiPerm):
2217 class HasPermissionAllApi(_BaseApiPerm):
2221 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2218 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2222 user_group_name=None):
2219 user_group_name=None):
2223 if self.required_perms.issubset(perm_defs.get('global')):
2220 if self.required_perms.issubset(perm_defs.get('global')):
2224 return True
2221 return True
2225 return False
2222 return False
2226
2223
2227
2224
2228 class HasPermissionAnyApi(_BaseApiPerm):
2225 class HasPermissionAnyApi(_BaseApiPerm):
2229 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2226 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2230 user_group_name=None):
2227 user_group_name=None):
2231 if self.required_perms.intersection(perm_defs.get('global')):
2228 if self.required_perms.intersection(perm_defs.get('global')):
2232 return True
2229 return True
2233 return False
2230 return False
2234
2231
2235
2232
2236 class HasRepoPermissionAllApi(_BaseApiPerm):
2233 class HasRepoPermissionAllApi(_BaseApiPerm):
2237 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2234 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2238 user_group_name=None):
2235 user_group_name=None):
2239 try:
2236 try:
2240 _user_perms = {perm_defs['repositories'][repo_name]}
2237 _user_perms = {perm_defs['repositories'][repo_name]}
2241 except KeyError:
2238 except KeyError:
2242 log.warning(traceback.format_exc())
2239 log.warning(traceback.format_exc())
2243 return False
2240 return False
2244 if self.required_perms.issubset(_user_perms):
2241 if self.required_perms.issubset(_user_perms):
2245 return True
2242 return True
2246 return False
2243 return False
2247
2244
2248
2245
2249 class HasRepoPermissionAnyApi(_BaseApiPerm):
2246 class HasRepoPermissionAnyApi(_BaseApiPerm):
2250 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2247 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2251 user_group_name=None):
2248 user_group_name=None):
2252 try:
2249 try:
2253 _user_perms = {perm_defs['repositories'][repo_name]}
2250 _user_perms = {perm_defs['repositories'][repo_name]}
2254 except KeyError:
2251 except KeyError:
2255 log.warning(traceback.format_exc())
2252 log.warning(traceback.format_exc())
2256 return False
2253 return False
2257 if self.required_perms.intersection(_user_perms):
2254 if self.required_perms.intersection(_user_perms):
2258 return True
2255 return True
2259 return False
2256 return False
2260
2257
2261
2258
2262 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2259 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2263 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2260 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2264 user_group_name=None):
2261 user_group_name=None):
2265 try:
2262 try:
2266 _user_perms = {perm_defs['repositories_groups'][group_name]}
2263 _user_perms = {perm_defs['repositories_groups'][group_name]}
2267 except KeyError:
2264 except KeyError:
2268 log.warning(traceback.format_exc())
2265 log.warning(traceback.format_exc())
2269 return False
2266 return False
2270 if self.required_perms.intersection(_user_perms):
2267 if self.required_perms.intersection(_user_perms):
2271 return True
2268 return True
2272 return False
2269 return False
2273
2270
2274
2271
2275 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2272 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2276 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2273 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2277 user_group_name=None):
2274 user_group_name=None):
2278 try:
2275 try:
2279 _user_perms = {perm_defs['repositories_groups'][group_name]}
2276 _user_perms = {perm_defs['repositories_groups'][group_name]}
2280 except KeyError:
2277 except KeyError:
2281 log.warning(traceback.format_exc())
2278 log.warning(traceback.format_exc())
2282 return False
2279 return False
2283 if self.required_perms.issubset(_user_perms):
2280 if self.required_perms.issubset(_user_perms):
2284 return True
2281 return True
2285 return False
2282 return False
2286
2283
2287
2284
2288 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2285 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2289 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2286 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2290 user_group_name=None):
2287 user_group_name=None):
2291 try:
2288 try:
2292 _user_perms = {perm_defs['user_groups'][user_group_name]}
2289 _user_perms = {perm_defs['user_groups'][user_group_name]}
2293 except KeyError:
2290 except KeyError:
2294 log.warning(traceback.format_exc())
2291 log.warning(traceback.format_exc())
2295 return False
2292 return False
2296 if self.required_perms.intersection(_user_perms):
2293 if self.required_perms.intersection(_user_perms):
2297 return True
2294 return True
2298 return False
2295 return False
2299
2296
2300
2297
2301 def check_ip_access(source_ip, allowed_ips=None):
2298 def check_ip_access(source_ip, allowed_ips=None):
2302 """
2299 """
2303 Checks if source_ip is a subnet of any of allowed_ips.
2300 Checks if source_ip is a subnet of any of allowed_ips.
2304
2301
2305 :param source_ip:
2302 :param source_ip:
2306 :param allowed_ips: list of allowed ips together with mask
2303 :param allowed_ips: list of allowed ips together with mask
2307 """
2304 """
2308 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
2305 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
2309 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2306 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2310 if isinstance(allowed_ips, (tuple, list, set)):
2307 if isinstance(allowed_ips, (tuple, list, set)):
2311 for ip in allowed_ips:
2308 for ip in allowed_ips:
2312 ip = safe_unicode(ip)
2309 ip = safe_unicode(ip)
2313 try:
2310 try:
2314 network_address = ipaddress.ip_network(ip, strict=False)
2311 network_address = ipaddress.ip_network(ip, strict=False)
2315 if source_ip_address in network_address:
2312 if source_ip_address in network_address:
2316 log.debug('IP %s is network %s' %
2313 log.debug('IP %s is network %s', source_ip_address, network_address)
2317 (source_ip_address, network_address))
2318 return True
2314 return True
2319 # for any case we cannot determine the IP, don't crash just
2315 # for any case we cannot determine the IP, don't crash just
2320 # skip it and log as error, we want to say forbidden still when
2316 # skip it and log as error, we want to say forbidden still when
2321 # sending bad IP
2317 # sending bad IP
2322 except Exception:
2318 except Exception:
2323 log.error(traceback.format_exc())
2319 log.error(traceback.format_exc())
2324 continue
2320 continue
2325 return False
2321 return False
2326
2322
2327
2323
2328 def get_cython_compat_decorator(wrapper, func):
2324 def get_cython_compat_decorator(wrapper, func):
2329 """
2325 """
2330 Creates a cython compatible decorator. The previously used
2326 Creates a cython compatible decorator. The previously used
2331 decorator.decorator() function seems to be incompatible with cython.
2327 decorator.decorator() function seems to be incompatible with cython.
2332
2328
2333 :param wrapper: __wrapper method of the decorator class
2329 :param wrapper: __wrapper method of the decorator class
2334 :param func: decorated function
2330 :param func: decorated function
2335 """
2331 """
2336 @wraps(func)
2332 @wraps(func)
2337 def local_wrapper(*args, **kwds):
2333 def local_wrapper(*args, **kwds):
2338 return wrapper(func, *args, **kwds)
2334 return wrapper(func, *args, **kwds)
2339 local_wrapper.__wrapped__ = func
2335 local_wrapper.__wrapped__ = func
2340 return local_wrapper
2336 return local_wrapper
2341
2337
2342
2338
@@ -1,550 +1,550 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 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36
36
37 import rhodecode
37 import rhodecode
38 from rhodecode.authentication.base import VCS_TYPE
38 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.lib import auth, utils2
39 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import helpers as h
40 from rhodecode.lib import helpers as h
41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 from rhodecode.lib.utils2 import (
44 from rhodecode.lib.utils2 import (
45 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
45 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
46 from rhodecode.model.db import Repository, User, ChangesetComment
46 from rhodecode.model.db import Repository, User, ChangesetComment
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 def _filter_proxy(ip):
53 def _filter_proxy(ip):
54 """
54 """
55 Passed in IP addresses in HEADERS can be in a special format of multiple
55 Passed in IP addresses in HEADERS can be in a special format of multiple
56 ips. Those comma separated IPs are passed from various proxies in the
56 ips. Those comma separated IPs are passed from various proxies in the
57 chain of request processing. The left-most being the original client.
57 chain of request processing. The left-most being the original client.
58 We only care about the first IP which came from the org. client.
58 We only care about the first IP which came from the org. client.
59
59
60 :param ip: ip string from headers
60 :param ip: ip string from headers
61 """
61 """
62 if ',' in ip:
62 if ',' in ip:
63 _ips = ip.split(',')
63 _ips = ip.split(',')
64 _first_ip = _ips[0].strip()
64 _first_ip = _ips[0].strip()
65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
66 return _first_ip
66 return _first_ip
67 return ip
67 return ip
68
68
69
69
70 def _filter_port(ip):
70 def _filter_port(ip):
71 """
71 """
72 Removes a port from ip, there are 4 main cases to handle here.
72 Removes a port from ip, there are 4 main cases to handle here.
73 - ipv4 eg. 127.0.0.1
73 - ipv4 eg. 127.0.0.1
74 - ipv6 eg. ::1
74 - ipv6 eg. ::1
75 - ipv4+port eg. 127.0.0.1:8080
75 - ipv4+port eg. 127.0.0.1:8080
76 - ipv6+port eg. [::1]:8080
76 - ipv6+port eg. [::1]:8080
77
77
78 :param ip:
78 :param ip:
79 """
79 """
80 def is_ipv6(ip_addr):
80 def is_ipv6(ip_addr):
81 if hasattr(socket, 'inet_pton'):
81 if hasattr(socket, 'inet_pton'):
82 try:
82 try:
83 socket.inet_pton(socket.AF_INET6, ip_addr)
83 socket.inet_pton(socket.AF_INET6, ip_addr)
84 except socket.error:
84 except socket.error:
85 return False
85 return False
86 else:
86 else:
87 # fallback to ipaddress
87 # fallback to ipaddress
88 try:
88 try:
89 ipaddress.IPv6Address(safe_unicode(ip_addr))
89 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 except Exception:
90 except Exception:
91 return False
91 return False
92 return True
92 return True
93
93
94 if ':' not in ip: # must be ipv4 pure ip
94 if ':' not in ip: # must be ipv4 pure ip
95 return ip
95 return ip
96
96
97 if '[' in ip and ']' in ip: # ipv6 with port
97 if '[' in ip and ']' in ip: # ipv6 with port
98 return ip.split(']')[0][1:].lower()
98 return ip.split(']')[0][1:].lower()
99
99
100 # must be ipv6 or ipv4 with port
100 # must be ipv6 or ipv4 with port
101 if is_ipv6(ip):
101 if is_ipv6(ip):
102 return ip
102 return ip
103 else:
103 else:
104 ip, _port = ip.split(':')[:2] # means ipv4+port
104 ip, _port = ip.split(':')[:2] # means ipv4+port
105 return ip
105 return ip
106
106
107
107
108 def get_ip_addr(environ):
108 def get_ip_addr(environ):
109 proxy_key = 'HTTP_X_REAL_IP'
109 proxy_key = 'HTTP_X_REAL_IP'
110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 def_key = 'REMOTE_ADDR'
111 def_key = 'REMOTE_ADDR'
112 _filters = lambda x: _filter_port(_filter_proxy(x))
112 _filters = lambda x: _filter_port(_filter_proxy(x))
113
113
114 ip = environ.get(proxy_key)
114 ip = environ.get(proxy_key)
115 if ip:
115 if ip:
116 return _filters(ip)
116 return _filters(ip)
117
117
118 ip = environ.get(proxy_key2)
118 ip = environ.get(proxy_key2)
119 if ip:
119 if ip:
120 return _filters(ip)
120 return _filters(ip)
121
121
122 ip = environ.get(def_key, '0.0.0.0')
122 ip = environ.get(def_key, '0.0.0.0')
123 return _filters(ip)
123 return _filters(ip)
124
124
125
125
126 def get_server_ip_addr(environ, log_errors=True):
126 def get_server_ip_addr(environ, log_errors=True):
127 hostname = environ.get('SERVER_NAME')
127 hostname = environ.get('SERVER_NAME')
128 try:
128 try:
129 return socket.gethostbyname(hostname)
129 return socket.gethostbyname(hostname)
130 except Exception as e:
130 except Exception as e:
131 if log_errors:
131 if log_errors:
132 # in some cases this lookup is not possible, and we don't want to
132 # in some cases this lookup is not possible, and we don't want to
133 # make it an exception in logs
133 # make it an exception in logs
134 log.exception('Could not retrieve server ip address: %s', e)
134 log.exception('Could not retrieve server ip address: %s', e)
135 return hostname
135 return hostname
136
136
137
137
138 def get_server_port(environ):
138 def get_server_port(environ):
139 return environ.get('SERVER_PORT')
139 return environ.get('SERVER_PORT')
140
140
141
141
142 def get_access_path(environ):
142 def get_access_path(environ):
143 path = environ.get('PATH_INFO')
143 path = environ.get('PATH_INFO')
144 org_req = environ.get('pylons.original_request')
144 org_req = environ.get('pylons.original_request')
145 if org_req:
145 if org_req:
146 path = org_req.environ.get('PATH_INFO')
146 path = org_req.environ.get('PATH_INFO')
147 return path
147 return path
148
148
149
149
150 def get_user_agent(environ):
150 def get_user_agent(environ):
151 return environ.get('HTTP_USER_AGENT')
151 return environ.get('HTTP_USER_AGENT')
152
152
153
153
154 def vcs_operation_context(
154 def vcs_operation_context(
155 environ, repo_name, username, action, scm, check_locking=True,
155 environ, repo_name, username, action, scm, check_locking=True,
156 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
156 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
157 """
157 """
158 Generate the context for a vcs operation, e.g. push or pull.
158 Generate the context for a vcs operation, e.g. push or pull.
159
159
160 This context is passed over the layers so that hooks triggered by the
160 This context is passed over the layers so that hooks triggered by the
161 vcs operation know details like the user, the user's IP address etc.
161 vcs operation know details like the user, the user's IP address etc.
162
162
163 :param check_locking: Allows to switch of the computation of the locking
163 :param check_locking: Allows to switch of the computation of the locking
164 data. This serves mainly the need of the simplevcs middleware to be
164 data. This serves mainly the need of the simplevcs middleware to be
165 able to disable this for certain operations.
165 able to disable this for certain operations.
166
166
167 """
167 """
168 # Tri-state value: False: unlock, None: nothing, True: lock
168 # Tri-state value: False: unlock, None: nothing, True: lock
169 make_lock = None
169 make_lock = None
170 locked_by = [None, None, None]
170 locked_by = [None, None, None]
171 is_anonymous = username == User.DEFAULT_USER
171 is_anonymous = username == User.DEFAULT_USER
172 user = User.get_by_username(username)
172 user = User.get_by_username(username)
173 if not is_anonymous and check_locking:
173 if not is_anonymous and check_locking:
174 log.debug('Checking locking on repository "%s"', repo_name)
174 log.debug('Checking locking on repository "%s"', repo_name)
175 repo = Repository.get_by_repo_name(repo_name)
175 repo = Repository.get_by_repo_name(repo_name)
176 make_lock, __, locked_by = repo.get_locking_state(
176 make_lock, __, locked_by = repo.get_locking_state(
177 action, user.user_id)
177 action, user.user_id)
178 user_id = user.user_id
178 user_id = user.user_id
179 settings_model = VcsSettingsModel(repo=repo_name)
179 settings_model = VcsSettingsModel(repo=repo_name)
180 ui_settings = settings_model.get_ui_settings()
180 ui_settings = settings_model.get_ui_settings()
181
181
182 # NOTE(marcink): This should be also in sync with
182 # NOTE(marcink): This should be also in sync with
183 # rhodecode/apps/ssh_support/lib/backends/base.py:update_enviroment scm_data
183 # rhodecode/apps/ssh_support/lib/backends/base.py:update_enviroment scm_data
184 scm_data = {
184 scm_data = {
185 'ip': get_ip_addr(environ),
185 'ip': get_ip_addr(environ),
186 'username': username,
186 'username': username,
187 'user_id': user_id,
187 'user_id': user_id,
188 'action': action,
188 'action': action,
189 'repository': repo_name,
189 'repository': repo_name,
190 'scm': scm,
190 'scm': scm,
191 'config': rhodecode.CONFIG['__file__'],
191 'config': rhodecode.CONFIG['__file__'],
192 'make_lock': make_lock,
192 'make_lock': make_lock,
193 'locked_by': locked_by,
193 'locked_by': locked_by,
194 'server_url': utils2.get_server_url(environ),
194 'server_url': utils2.get_server_url(environ),
195 'user_agent': get_user_agent(environ),
195 'user_agent': get_user_agent(environ),
196 'hooks': get_enabled_hook_classes(ui_settings),
196 'hooks': get_enabled_hook_classes(ui_settings),
197 'is_shadow_repo': is_shadow_repo,
197 'is_shadow_repo': is_shadow_repo,
198 'detect_force_push': detect_force_push,
198 'detect_force_push': detect_force_push,
199 'check_branch_perms': check_branch_perms,
199 'check_branch_perms': check_branch_perms,
200 }
200 }
201 return scm_data
201 return scm_data
202
202
203
203
204 class BasicAuth(AuthBasicAuthenticator):
204 class BasicAuth(AuthBasicAuthenticator):
205
205
206 def __init__(self, realm, authfunc, registry, auth_http_code=None,
206 def __init__(self, realm, authfunc, registry, auth_http_code=None,
207 initial_call_detection=False, acl_repo_name=None):
207 initial_call_detection=False, acl_repo_name=None):
208 self.realm = realm
208 self.realm = realm
209 self.initial_call = initial_call_detection
209 self.initial_call = initial_call_detection
210 self.authfunc = authfunc
210 self.authfunc = authfunc
211 self.registry = registry
211 self.registry = registry
212 self.acl_repo_name = acl_repo_name
212 self.acl_repo_name = acl_repo_name
213 self._rc_auth_http_code = auth_http_code
213 self._rc_auth_http_code = auth_http_code
214
214
215 def _get_response_from_code(self, http_code):
215 def _get_response_from_code(self, http_code):
216 try:
216 try:
217 return get_exception(safe_int(http_code))
217 return get_exception(safe_int(http_code))
218 except Exception:
218 except Exception:
219 log.exception('Failed to fetch response for code %s' % http_code)
219 log.exception('Failed to fetch response for code %s', http_code)
220 return HTTPForbidden
220 return HTTPForbidden
221
221
222 def get_rc_realm(self):
222 def get_rc_realm(self):
223 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
223 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
224
224
225 def build_authentication(self):
225 def build_authentication(self):
226 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
226 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
227 if self._rc_auth_http_code and not self.initial_call:
227 if self._rc_auth_http_code and not self.initial_call:
228 # return alternative HTTP code if alternative http return code
228 # return alternative HTTP code if alternative http return code
229 # is specified in RhodeCode config, but ONLY if it's not the
229 # is specified in RhodeCode config, but ONLY if it's not the
230 # FIRST call
230 # FIRST call
231 custom_response_klass = self._get_response_from_code(
231 custom_response_klass = self._get_response_from_code(
232 self._rc_auth_http_code)
232 self._rc_auth_http_code)
233 return custom_response_klass(headers=head)
233 return custom_response_klass(headers=head)
234 return HTTPUnauthorized(headers=head)
234 return HTTPUnauthorized(headers=head)
235
235
236 def authenticate(self, environ):
236 def authenticate(self, environ):
237 authorization = AUTHORIZATION(environ)
237 authorization = AUTHORIZATION(environ)
238 if not authorization:
238 if not authorization:
239 return self.build_authentication()
239 return self.build_authentication()
240 (authmeth, auth) = authorization.split(' ', 1)
240 (authmeth, auth) = authorization.split(' ', 1)
241 if 'basic' != authmeth.lower():
241 if 'basic' != authmeth.lower():
242 return self.build_authentication()
242 return self.build_authentication()
243 auth = auth.strip().decode('base64')
243 auth = auth.strip().decode('base64')
244 _parts = auth.split(':', 1)
244 _parts = auth.split(':', 1)
245 if len(_parts) == 2:
245 if len(_parts) == 2:
246 username, password = _parts
246 username, password = _parts
247 auth_data = self.authfunc(
247 auth_data = self.authfunc(
248 username, password, environ, VCS_TYPE,
248 username, password, environ, VCS_TYPE,
249 registry=self.registry, acl_repo_name=self.acl_repo_name)
249 registry=self.registry, acl_repo_name=self.acl_repo_name)
250 if auth_data:
250 if auth_data:
251 return {'username': username, 'auth_data': auth_data}
251 return {'username': username, 'auth_data': auth_data}
252 if username and password:
252 if username and password:
253 # we mark that we actually executed authentication once, at
253 # we mark that we actually executed authentication once, at
254 # that point we can use the alternative auth code
254 # that point we can use the alternative auth code
255 self.initial_call = False
255 self.initial_call = False
256
256
257 return self.build_authentication()
257 return self.build_authentication()
258
258
259 __call__ = authenticate
259 __call__ = authenticate
260
260
261
261
262 def calculate_version_hash(config):
262 def calculate_version_hash(config):
263 return sha1(
263 return sha1(
264 config.get('beaker.session.secret', '') +
264 config.get('beaker.session.secret', '') +
265 rhodecode.__version__)[:8]
265 rhodecode.__version__)[:8]
266
266
267
267
268 def get_current_lang(request):
268 def get_current_lang(request):
269 # NOTE(marcink): remove after pyramid move
269 # NOTE(marcink): remove after pyramid move
270 try:
270 try:
271 return translation.get_lang()[0]
271 return translation.get_lang()[0]
272 except:
272 except:
273 pass
273 pass
274
274
275 return getattr(request, '_LOCALE_', request.locale_name)
275 return getattr(request, '_LOCALE_', request.locale_name)
276
276
277
277
278 def attach_context_attributes(context, request, user_id):
278 def attach_context_attributes(context, request, user_id):
279 """
279 """
280 Attach variables into template context called `c`.
280 Attach variables into template context called `c`.
281 """
281 """
282 config = request.registry.settings
282 config = request.registry.settings
283
283
284
284
285 rc_config = SettingsModel().get_all_settings(cache=True)
285 rc_config = SettingsModel().get_all_settings(cache=True)
286
286
287 context.rhodecode_version = rhodecode.__version__
287 context.rhodecode_version = rhodecode.__version__
288 context.rhodecode_edition = config.get('rhodecode.edition')
288 context.rhodecode_edition = config.get('rhodecode.edition')
289 # unique secret + version does not leak the version but keep consistency
289 # unique secret + version does not leak the version but keep consistency
290 context.rhodecode_version_hash = calculate_version_hash(config)
290 context.rhodecode_version_hash = calculate_version_hash(config)
291
291
292 # Default language set for the incoming request
292 # Default language set for the incoming request
293 context.language = get_current_lang(request)
293 context.language = get_current_lang(request)
294
294
295 # Visual options
295 # Visual options
296 context.visual = AttributeDict({})
296 context.visual = AttributeDict({})
297
297
298 # DB stored Visual Items
298 # DB stored Visual Items
299 context.visual.show_public_icon = str2bool(
299 context.visual.show_public_icon = str2bool(
300 rc_config.get('rhodecode_show_public_icon'))
300 rc_config.get('rhodecode_show_public_icon'))
301 context.visual.show_private_icon = str2bool(
301 context.visual.show_private_icon = str2bool(
302 rc_config.get('rhodecode_show_private_icon'))
302 rc_config.get('rhodecode_show_private_icon'))
303 context.visual.stylify_metatags = str2bool(
303 context.visual.stylify_metatags = str2bool(
304 rc_config.get('rhodecode_stylify_metatags'))
304 rc_config.get('rhodecode_stylify_metatags'))
305 context.visual.dashboard_items = safe_int(
305 context.visual.dashboard_items = safe_int(
306 rc_config.get('rhodecode_dashboard_items', 100))
306 rc_config.get('rhodecode_dashboard_items', 100))
307 context.visual.admin_grid_items = safe_int(
307 context.visual.admin_grid_items = safe_int(
308 rc_config.get('rhodecode_admin_grid_items', 100))
308 rc_config.get('rhodecode_admin_grid_items', 100))
309 context.visual.repository_fields = str2bool(
309 context.visual.repository_fields = str2bool(
310 rc_config.get('rhodecode_repository_fields'))
310 rc_config.get('rhodecode_repository_fields'))
311 context.visual.show_version = str2bool(
311 context.visual.show_version = str2bool(
312 rc_config.get('rhodecode_show_version'))
312 rc_config.get('rhodecode_show_version'))
313 context.visual.use_gravatar = str2bool(
313 context.visual.use_gravatar = str2bool(
314 rc_config.get('rhodecode_use_gravatar'))
314 rc_config.get('rhodecode_use_gravatar'))
315 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
315 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
316 context.visual.default_renderer = rc_config.get(
316 context.visual.default_renderer = rc_config.get(
317 'rhodecode_markup_renderer', 'rst')
317 'rhodecode_markup_renderer', 'rst')
318 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
318 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
319 context.visual.rhodecode_support_url = \
319 context.visual.rhodecode_support_url = \
320 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
320 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
321
321
322 context.visual.affected_files_cut_off = 60
322 context.visual.affected_files_cut_off = 60
323
323
324 context.pre_code = rc_config.get('rhodecode_pre_code')
324 context.pre_code = rc_config.get('rhodecode_pre_code')
325 context.post_code = rc_config.get('rhodecode_post_code')
325 context.post_code = rc_config.get('rhodecode_post_code')
326 context.rhodecode_name = rc_config.get('rhodecode_title')
326 context.rhodecode_name = rc_config.get('rhodecode_title')
327 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
327 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
328 # if we have specified default_encoding in the request, it has more
328 # if we have specified default_encoding in the request, it has more
329 # priority
329 # priority
330 if request.GET.get('default_encoding'):
330 if request.GET.get('default_encoding'):
331 context.default_encodings.insert(0, request.GET.get('default_encoding'))
331 context.default_encodings.insert(0, request.GET.get('default_encoding'))
332 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
332 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
333 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
333 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
334
334
335 # INI stored
335 # INI stored
336 context.labs_active = str2bool(
336 context.labs_active = str2bool(
337 config.get('labs_settings_active', 'false'))
337 config.get('labs_settings_active', 'false'))
338 context.ssh_enabled = str2bool(
338 context.ssh_enabled = str2bool(
339 config.get('ssh.generate_authorized_keyfile', 'false'))
339 config.get('ssh.generate_authorized_keyfile', 'false'))
340
340
341 context.visual.allow_repo_location_change = str2bool(
341 context.visual.allow_repo_location_change = str2bool(
342 config.get('allow_repo_location_change', True))
342 config.get('allow_repo_location_change', True))
343 context.visual.allow_custom_hooks_settings = str2bool(
343 context.visual.allow_custom_hooks_settings = str2bool(
344 config.get('allow_custom_hooks_settings', True))
344 config.get('allow_custom_hooks_settings', True))
345 context.debug_style = str2bool(config.get('debug_style', False))
345 context.debug_style = str2bool(config.get('debug_style', False))
346
346
347 context.rhodecode_instanceid = config.get('instance_id')
347 context.rhodecode_instanceid = config.get('instance_id')
348
348
349 context.visual.cut_off_limit_diff = safe_int(
349 context.visual.cut_off_limit_diff = safe_int(
350 config.get('cut_off_limit_diff'))
350 config.get('cut_off_limit_diff'))
351 context.visual.cut_off_limit_file = safe_int(
351 context.visual.cut_off_limit_file = safe_int(
352 config.get('cut_off_limit_file'))
352 config.get('cut_off_limit_file'))
353
353
354 # AppEnlight
354 # AppEnlight
355 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
355 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
356 context.appenlight_api_public_key = config.get(
356 context.appenlight_api_public_key = config.get(
357 'appenlight.api_public_key', '')
357 'appenlight.api_public_key', '')
358 context.appenlight_server_url = config.get('appenlight.server_url', '')
358 context.appenlight_server_url = config.get('appenlight.server_url', '')
359
359
360 # JS template context
360 # JS template context
361 context.template_context = {
361 context.template_context = {
362 'repo_name': None,
362 'repo_name': None,
363 'repo_type': None,
363 'repo_type': None,
364 'repo_landing_commit': None,
364 'repo_landing_commit': None,
365 'rhodecode_user': {
365 'rhodecode_user': {
366 'username': None,
366 'username': None,
367 'email': None,
367 'email': None,
368 'notification_status': False
368 'notification_status': False
369 },
369 },
370 'visual': {
370 'visual': {
371 'default_renderer': None
371 'default_renderer': None
372 },
372 },
373 'commit_data': {
373 'commit_data': {
374 'commit_id': None
374 'commit_id': None
375 },
375 },
376 'pull_request_data': {'pull_request_id': None},
376 'pull_request_data': {'pull_request_id': None},
377 'timeago': {
377 'timeago': {
378 'refresh_time': 120 * 1000,
378 'refresh_time': 120 * 1000,
379 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
379 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
380 },
380 },
381 'pyramid_dispatch': {
381 'pyramid_dispatch': {
382
382
383 },
383 },
384 'extra': {'plugins': {}}
384 'extra': {'plugins': {}}
385 }
385 }
386 # END CONFIG VARS
386 # END CONFIG VARS
387
387
388 diffmode = 'sideside'
388 diffmode = 'sideside'
389 if request.GET.get('diffmode'):
389 if request.GET.get('diffmode'):
390 if request.GET['diffmode'] == 'unified':
390 if request.GET['diffmode'] == 'unified':
391 diffmode = 'unified'
391 diffmode = 'unified'
392 elif request.session.get('diffmode'):
392 elif request.session.get('diffmode'):
393 diffmode = request.session['diffmode']
393 diffmode = request.session['diffmode']
394
394
395 context.diffmode = diffmode
395 context.diffmode = diffmode
396
396
397 if request.session.get('diffmode') != diffmode:
397 if request.session.get('diffmode') != diffmode:
398 request.session['diffmode'] = diffmode
398 request.session['diffmode'] = diffmode
399
399
400 context.csrf_token = auth.get_csrf_token(session=request.session)
400 context.csrf_token = auth.get_csrf_token(session=request.session)
401 context.backends = rhodecode.BACKENDS.keys()
401 context.backends = rhodecode.BACKENDS.keys()
402 context.backends.sort()
402 context.backends.sort()
403 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
403 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
404
404
405 # web case
405 # web case
406 if hasattr(request, 'user'):
406 if hasattr(request, 'user'):
407 context.auth_user = request.user
407 context.auth_user = request.user
408 context.rhodecode_user = request.user
408 context.rhodecode_user = request.user
409
409
410 # api case
410 # api case
411 if hasattr(request, 'rpc_user'):
411 if hasattr(request, 'rpc_user'):
412 context.auth_user = request.rpc_user
412 context.auth_user = request.rpc_user
413 context.rhodecode_user = request.rpc_user
413 context.rhodecode_user = request.rpc_user
414
414
415 # attach the whole call context to the request
415 # attach the whole call context to the request
416 request.call_context = context
416 request.call_context = context
417
417
418
418
419 def get_auth_user(request):
419 def get_auth_user(request):
420 environ = request.environ
420 environ = request.environ
421 session = request.session
421 session = request.session
422
422
423 ip_addr = get_ip_addr(environ)
423 ip_addr = get_ip_addr(environ)
424 # make sure that we update permissions each time we call controller
424 # make sure that we update permissions each time we call controller
425 _auth_token = (request.GET.get('auth_token', '') or
425 _auth_token = (request.GET.get('auth_token', '') or
426 request.GET.get('api_key', ''))
426 request.GET.get('api_key', ''))
427
427
428 if _auth_token:
428 if _auth_token:
429 # when using API_KEY we assume user exists, and
429 # when using API_KEY we assume user exists, and
430 # doesn't need auth based on cookies.
430 # doesn't need auth based on cookies.
431 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
431 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
432 authenticated = False
432 authenticated = False
433 else:
433 else:
434 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
434 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
435 try:
435 try:
436 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
436 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
437 ip_addr=ip_addr)
437 ip_addr=ip_addr)
438 except UserCreationError as e:
438 except UserCreationError as e:
439 h.flash(e, 'error')
439 h.flash(e, 'error')
440 # container auth or other auth functions that create users
440 # container auth or other auth functions that create users
441 # on the fly can throw this exception signaling that there's
441 # on the fly can throw this exception signaling that there's
442 # issue with user creation, explanation should be provided
442 # issue with user creation, explanation should be provided
443 # in Exception itself. We then create a simple blank
443 # in Exception itself. We then create a simple blank
444 # AuthUser
444 # AuthUser
445 auth_user = AuthUser(ip_addr=ip_addr)
445 auth_user = AuthUser(ip_addr=ip_addr)
446
446
447 # in case someone changes a password for user it triggers session
447 # in case someone changes a password for user it triggers session
448 # flush and forces a re-login
448 # flush and forces a re-login
449 if password_changed(auth_user, session):
449 if password_changed(auth_user, session):
450 session.invalidate()
450 session.invalidate()
451 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
451 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
452 auth_user = AuthUser(ip_addr=ip_addr)
452 auth_user = AuthUser(ip_addr=ip_addr)
453
453
454 authenticated = cookie_store.get('is_authenticated')
454 authenticated = cookie_store.get('is_authenticated')
455
455
456 if not auth_user.is_authenticated and auth_user.is_user_object:
456 if not auth_user.is_authenticated and auth_user.is_user_object:
457 # user is not authenticated and not empty
457 # user is not authenticated and not empty
458 auth_user.set_authenticated(authenticated)
458 auth_user.set_authenticated(authenticated)
459
459
460 return auth_user
460 return auth_user
461
461
462
462
463 def h_filter(s):
463 def h_filter(s):
464 """
464 """
465 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
465 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
466 we wrap this with additional functionality that converts None to empty
466 we wrap this with additional functionality that converts None to empty
467 strings
467 strings
468 """
468 """
469 if s is None:
469 if s is None:
470 return markupsafe.Markup()
470 return markupsafe.Markup()
471 return markupsafe.escape(s)
471 return markupsafe.escape(s)
472
472
473
473
474 def add_events_routes(config):
474 def add_events_routes(config):
475 """
475 """
476 Adds routing that can be used in events. Because some events are triggered
476 Adds routing that can be used in events. Because some events are triggered
477 outside of pyramid context, we need to bootstrap request with some
477 outside of pyramid context, we need to bootstrap request with some
478 routing registered
478 routing registered
479 """
479 """
480
480
481 from rhodecode.apps._base import ADMIN_PREFIX
481 from rhodecode.apps._base import ADMIN_PREFIX
482
482
483 config.add_route(name='home', pattern='/')
483 config.add_route(name='home', pattern='/')
484
484
485 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
485 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
486 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
486 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
487 config.add_route(name='repo_summary', pattern='/{repo_name}')
487 config.add_route(name='repo_summary', pattern='/{repo_name}')
488 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
488 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
489 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
489 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
490
490
491 config.add_route(name='pullrequest_show',
491 config.add_route(name='pullrequest_show',
492 pattern='/{repo_name}/pull-request/{pull_request_id}')
492 pattern='/{repo_name}/pull-request/{pull_request_id}')
493 config.add_route(name='pull_requests_global',
493 config.add_route(name='pull_requests_global',
494 pattern='/pull-request/{pull_request_id}')
494 pattern='/pull-request/{pull_request_id}')
495 config.add_route(name='repo_commit',
495 config.add_route(name='repo_commit',
496 pattern='/{repo_name}/changeset/{commit_id}')
496 pattern='/{repo_name}/changeset/{commit_id}')
497
497
498 config.add_route(name='repo_files',
498 config.add_route(name='repo_files',
499 pattern='/{repo_name}/files/{commit_id}/{f_path}')
499 pattern='/{repo_name}/files/{commit_id}/{f_path}')
500
500
501
501
502 def bootstrap_config(request):
502 def bootstrap_config(request):
503 import pyramid.testing
503 import pyramid.testing
504 registry = pyramid.testing.Registry('RcTestRegistry')
504 registry = pyramid.testing.Registry('RcTestRegistry')
505
505
506 config = pyramid.testing.setUp(registry=registry, request=request)
506 config = pyramid.testing.setUp(registry=registry, request=request)
507
507
508 # allow pyramid lookup in testing
508 # allow pyramid lookup in testing
509 config.include('pyramid_mako')
509 config.include('pyramid_mako')
510 config.include('pyramid_beaker')
510 config.include('pyramid_beaker')
511 config.include('rhodecode.lib.rc_cache')
511 config.include('rhodecode.lib.rc_cache')
512
512
513 add_events_routes(config)
513 add_events_routes(config)
514
514
515 return config
515 return config
516
516
517
517
518 def bootstrap_request(**kwargs):
518 def bootstrap_request(**kwargs):
519 import pyramid.testing
519 import pyramid.testing
520
520
521 class TestRequest(pyramid.testing.DummyRequest):
521 class TestRequest(pyramid.testing.DummyRequest):
522 application_url = kwargs.pop('application_url', 'http://example.com')
522 application_url = kwargs.pop('application_url', 'http://example.com')
523 host = kwargs.pop('host', 'example.com:80')
523 host = kwargs.pop('host', 'example.com:80')
524 domain = kwargs.pop('domain', 'example.com')
524 domain = kwargs.pop('domain', 'example.com')
525
525
526 def translate(self, msg):
526 def translate(self, msg):
527 return msg
527 return msg
528
528
529 def plularize(self, singular, plural, n):
529 def plularize(self, singular, plural, n):
530 return singular
530 return singular
531
531
532 def get_partial_renderer(self, tmpl_name):
532 def get_partial_renderer(self, tmpl_name):
533
533
534 from rhodecode.lib.partial_renderer import get_partial_renderer
534 from rhodecode.lib.partial_renderer import get_partial_renderer
535 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
535 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
536
536
537 _call_context = {}
537 _call_context = {}
538 @property
538 @property
539 def call_context(self):
539 def call_context(self):
540 return self._call_context
540 return self._call_context
541
541
542 class TestDummySession(pyramid.testing.DummySession):
542 class TestDummySession(pyramid.testing.DummySession):
543 def save(*arg, **kw):
543 def save(*arg, **kw):
544 pass
544 pass
545
545
546 request = TestRequest(**kwargs)
546 request = TestRequest(**kwargs)
547 request.session = TestDummySession()
547 request.session = TestDummySession()
548
548
549 return request
549 return request
550
550
@@ -1,745 +1,745 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 logging
21 import logging
22 import difflib
22 import difflib
23 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from pygments.lexers.special import TextLexer, Token
27 from pygments.lexers.special import TextLexer, Token
28
28
29 from rhodecode.lib.helpers import (
29 from rhodecode.lib.helpers import (
30 get_lexer_for_filenode, html_escape, get_custom_lexer)
30 get_lexer_for_filenode, html_escape, get_custom_lexer)
31 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict
31 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict
32 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
33 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
34 from rhodecode.lib.diff_match_patch import diff_match_patch
34 from rhodecode.lib.diff_match_patch import diff_match_patch
35 from rhodecode.lib.diffs import LimitedDiffContainer
35 from rhodecode.lib.diffs import LimitedDiffContainer
36 from pygments.lexers import get_lexer_by_name
36 from pygments.lexers import get_lexer_by_name
37
37
38 plain_text_lexer = get_lexer_by_name(
38 plain_text_lexer = get_lexer_by_name(
39 'text', stripall=False, stripnl=False, ensurenl=False)
39 'text', stripall=False, stripnl=False, ensurenl=False)
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def filenode_as_lines_tokens(filenode, lexer=None):
45 def filenode_as_lines_tokens(filenode, lexer=None):
46 org_lexer = lexer
46 org_lexer = lexer
47 lexer = lexer or get_lexer_for_filenode(filenode)
47 lexer = lexer or get_lexer_for_filenode(filenode)
48 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
48 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
49 lexer, filenode, org_lexer)
49 lexer, filenode, org_lexer)
50 tokens = tokenize_string(filenode.content, lexer)
50 tokens = tokenize_string(filenode.content, lexer)
51 lines = split_token_stream(tokens)
51 lines = split_token_stream(tokens)
52 rv = list(lines)
52 rv = list(lines)
53 return rv
53 return rv
54
54
55
55
56 def tokenize_string(content, lexer):
56 def tokenize_string(content, lexer):
57 """
57 """
58 Use pygments to tokenize some content based on a lexer
58 Use pygments to tokenize some content based on a lexer
59 ensuring all original new lines and whitespace is preserved
59 ensuring all original new lines and whitespace is preserved
60 """
60 """
61
61
62 lexer.stripall = False
62 lexer.stripall = False
63 lexer.stripnl = False
63 lexer.stripnl = False
64 lexer.ensurenl = False
64 lexer.ensurenl = False
65
65
66 if isinstance(lexer, TextLexer):
66 if isinstance(lexer, TextLexer):
67 lexed = [(Token.Text, content)]
67 lexed = [(Token.Text, content)]
68 else:
68 else:
69 lexed = lex(content, lexer)
69 lexed = lex(content, lexer)
70
70
71 for token_type, token_text in lexed:
71 for token_type, token_text in lexed:
72 yield pygment_token_class(token_type), token_text
72 yield pygment_token_class(token_type), token_text
73
73
74
74
75 def split_token_stream(tokens):
75 def split_token_stream(tokens):
76 """
76 """
77 Take a list of (TokenType, text) tuples and split them by a string
77 Take a list of (TokenType, text) tuples and split them by a string
78
78
79 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
79 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
80 [(TEXT, 'some'), (TEXT, 'text'),
80 [(TEXT, 'some'), (TEXT, 'text'),
81 (TEXT, 'more'), (TEXT, 'text')]
81 (TEXT, 'more'), (TEXT, 'text')]
82 """
82 """
83
83
84 buffer = []
84 buffer = []
85 for token_class, token_text in tokens:
85 for token_class, token_text in tokens:
86 parts = token_text.split('\n')
86 parts = token_text.split('\n')
87 for part in parts[:-1]:
87 for part in parts[:-1]:
88 buffer.append((token_class, part))
88 buffer.append((token_class, part))
89 yield buffer
89 yield buffer
90 buffer = []
90 buffer = []
91
91
92 buffer.append((token_class, parts[-1]))
92 buffer.append((token_class, parts[-1]))
93
93
94 if buffer:
94 if buffer:
95 yield buffer
95 yield buffer
96
96
97
97
98 def filenode_as_annotated_lines_tokens(filenode):
98 def filenode_as_annotated_lines_tokens(filenode):
99 """
99 """
100 Take a file node and return a list of annotations => lines, if no annotation
100 Take a file node and return a list of annotations => lines, if no annotation
101 is found, it will be None.
101 is found, it will be None.
102
102
103 eg:
103 eg:
104
104
105 [
105 [
106 (annotation1, [
106 (annotation1, [
107 (1, line1_tokens_list),
107 (1, line1_tokens_list),
108 (2, line2_tokens_list),
108 (2, line2_tokens_list),
109 ]),
109 ]),
110 (annotation2, [
110 (annotation2, [
111 (3, line1_tokens_list),
111 (3, line1_tokens_list),
112 ]),
112 ]),
113 (None, [
113 (None, [
114 (4, line1_tokens_list),
114 (4, line1_tokens_list),
115 ]),
115 ]),
116 (annotation1, [
116 (annotation1, [
117 (5, line1_tokens_list),
117 (5, line1_tokens_list),
118 (6, line2_tokens_list),
118 (6, line2_tokens_list),
119 ])
119 ])
120 ]
120 ]
121 """
121 """
122
122
123 commit_cache = {} # cache commit_getter lookups
123 commit_cache = {} # cache commit_getter lookups
124
124
125 def _get_annotation(commit_id, commit_getter):
125 def _get_annotation(commit_id, commit_getter):
126 if commit_id not in commit_cache:
126 if commit_id not in commit_cache:
127 commit_cache[commit_id] = commit_getter()
127 commit_cache[commit_id] = commit_getter()
128 return commit_cache[commit_id]
128 return commit_cache[commit_id]
129
129
130 annotation_lookup = {
130 annotation_lookup = {
131 line_no: _get_annotation(commit_id, commit_getter)
131 line_no: _get_annotation(commit_id, commit_getter)
132 for line_no, commit_id, commit_getter, line_content
132 for line_no, commit_id, commit_getter, line_content
133 in filenode.annotate
133 in filenode.annotate
134 }
134 }
135
135
136 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
136 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
137 for line_no, tokens
137 for line_no, tokens
138 in enumerate(filenode_as_lines_tokens(filenode), 1))
138 in enumerate(filenode_as_lines_tokens(filenode), 1))
139
139
140 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
140 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
141
141
142 for annotation, group in grouped_annotations_lines:
142 for annotation, group in grouped_annotations_lines:
143 yield (
143 yield (
144 annotation, [(line_no, tokens)
144 annotation, [(line_no, tokens)
145 for (_, line_no, tokens) in group]
145 for (_, line_no, tokens) in group]
146 )
146 )
147
147
148
148
149 def render_tokenstream(tokenstream):
149 def render_tokenstream(tokenstream):
150 result = []
150 result = []
151 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
151 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
152
152
153 if token_class:
153 if token_class:
154 result.append(u'<span class="%s">' % token_class)
154 result.append(u'<span class="%s">' % token_class)
155 else:
155 else:
156 result.append(u'<span>')
156 result.append(u'<span>')
157
157
158 for op_tag, token_text in token_ops_texts:
158 for op_tag, token_text in token_ops_texts:
159
159
160 if op_tag:
160 if op_tag:
161 result.append(u'<%s>' % op_tag)
161 result.append(u'<%s>' % op_tag)
162
162
163 escaped_text = html_escape(token_text)
163 escaped_text = html_escape(token_text)
164
164
165 # TODO: dan: investigate showing hidden characters like space/nl/tab
165 # TODO: dan: investigate showing hidden characters like space/nl/tab
166 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
166 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
167 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
167 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
168 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
168 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
169
169
170 result.append(escaped_text)
170 result.append(escaped_text)
171
171
172 if op_tag:
172 if op_tag:
173 result.append(u'</%s>' % op_tag)
173 result.append(u'</%s>' % op_tag)
174
174
175 result.append(u'</span>')
175 result.append(u'</span>')
176
176
177 html = ''.join(result)
177 html = ''.join(result)
178 return html
178 return html
179
179
180
180
181 def rollup_tokenstream(tokenstream):
181 def rollup_tokenstream(tokenstream):
182 """
182 """
183 Group a token stream of the format:
183 Group a token stream of the format:
184
184
185 ('class', 'op', 'text')
185 ('class', 'op', 'text')
186 or
186 or
187 ('class', 'text')
187 ('class', 'text')
188
188
189 into
189 into
190
190
191 [('class1',
191 [('class1',
192 [('op1', 'text'),
192 [('op1', 'text'),
193 ('op2', 'text')]),
193 ('op2', 'text')]),
194 ('class2',
194 ('class2',
195 [('op3', 'text')])]
195 [('op3', 'text')])]
196
196
197 This is used to get the minimal tags necessary when
197 This is used to get the minimal tags necessary when
198 rendering to html eg for a token stream ie.
198 rendering to html eg for a token stream ie.
199
199
200 <span class="A"><ins>he</ins>llo</span>
200 <span class="A"><ins>he</ins>llo</span>
201 vs
201 vs
202 <span class="A"><ins>he</ins></span><span class="A">llo</span>
202 <span class="A"><ins>he</ins></span><span class="A">llo</span>
203
203
204 If a 2 tuple is passed in, the output op will be an empty string.
204 If a 2 tuple is passed in, the output op will be an empty string.
205
205
206 eg:
206 eg:
207
207
208 >>> rollup_tokenstream([('classA', '', 'h'),
208 >>> rollup_tokenstream([('classA', '', 'h'),
209 ('classA', 'del', 'ell'),
209 ('classA', 'del', 'ell'),
210 ('classA', '', 'o'),
210 ('classA', '', 'o'),
211 ('classB', '', ' '),
211 ('classB', '', ' '),
212 ('classA', '', 'the'),
212 ('classA', '', 'the'),
213 ('classA', '', 're'),
213 ('classA', '', 're'),
214 ])
214 ])
215
215
216 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
216 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
217 ('classB', [('', ' ')],
217 ('classB', [('', ' ')],
218 ('classA', [('', 'there')]]
218 ('classA', [('', 'there')]]
219
219
220 """
220 """
221 if tokenstream and len(tokenstream[0]) == 2:
221 if tokenstream and len(tokenstream[0]) == 2:
222 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
222 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
223
223
224 result = []
224 result = []
225 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
225 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
226 ops = []
226 ops = []
227 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
227 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
228 text_buffer = []
228 text_buffer = []
229 for t_class, t_op, t_text in token_text_list:
229 for t_class, t_op, t_text in token_text_list:
230 text_buffer.append(t_text)
230 text_buffer.append(t_text)
231 ops.append((token_op, ''.join(text_buffer)))
231 ops.append((token_op, ''.join(text_buffer)))
232 result.append((token_class, ops))
232 result.append((token_class, ops))
233 return result
233 return result
234
234
235
235
236 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
236 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
237 """
237 """
238 Converts a list of (token_class, token_text) tuples to a list of
238 Converts a list of (token_class, token_text) tuples to a list of
239 (token_class, token_op, token_text) tuples where token_op is one of
239 (token_class, token_op, token_text) tuples where token_op is one of
240 ('ins', 'del', '')
240 ('ins', 'del', '')
241
241
242 :param old_tokens: list of (token_class, token_text) tuples of old line
242 :param old_tokens: list of (token_class, token_text) tuples of old line
243 :param new_tokens: list of (token_class, token_text) tuples of new line
243 :param new_tokens: list of (token_class, token_text) tuples of new line
244 :param use_diff_match_patch: boolean, will use google's diff match patch
244 :param use_diff_match_patch: boolean, will use google's diff match patch
245 library which has options to 'smooth' out the character by character
245 library which has options to 'smooth' out the character by character
246 differences making nicer ins/del blocks
246 differences making nicer ins/del blocks
247 """
247 """
248
248
249 old_tokens_result = []
249 old_tokens_result = []
250 new_tokens_result = []
250 new_tokens_result = []
251
251
252 similarity = difflib.SequenceMatcher(None,
252 similarity = difflib.SequenceMatcher(None,
253 ''.join(token_text for token_class, token_text in old_tokens),
253 ''.join(token_text for token_class, token_text in old_tokens),
254 ''.join(token_text for token_class, token_text in new_tokens)
254 ''.join(token_text for token_class, token_text in new_tokens)
255 ).ratio()
255 ).ratio()
256
256
257 if similarity < 0.6: # return, the blocks are too different
257 if similarity < 0.6: # return, the blocks are too different
258 for token_class, token_text in old_tokens:
258 for token_class, token_text in old_tokens:
259 old_tokens_result.append((token_class, '', token_text))
259 old_tokens_result.append((token_class, '', token_text))
260 for token_class, token_text in new_tokens:
260 for token_class, token_text in new_tokens:
261 new_tokens_result.append((token_class, '', token_text))
261 new_tokens_result.append((token_class, '', token_text))
262 return old_tokens_result, new_tokens_result, similarity
262 return old_tokens_result, new_tokens_result, similarity
263
263
264 token_sequence_matcher = difflib.SequenceMatcher(None,
264 token_sequence_matcher = difflib.SequenceMatcher(None,
265 [x[1] for x in old_tokens],
265 [x[1] for x in old_tokens],
266 [x[1] for x in new_tokens])
266 [x[1] for x in new_tokens])
267
267
268 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
268 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
269 # check the differences by token block types first to give a more
269 # check the differences by token block types first to give a more
270 # nicer "block" level replacement vs character diffs
270 # nicer "block" level replacement vs character diffs
271
271
272 if tag == 'equal':
272 if tag == 'equal':
273 for token_class, token_text in old_tokens[o1:o2]:
273 for token_class, token_text in old_tokens[o1:o2]:
274 old_tokens_result.append((token_class, '', token_text))
274 old_tokens_result.append((token_class, '', token_text))
275 for token_class, token_text in new_tokens[n1:n2]:
275 for token_class, token_text in new_tokens[n1:n2]:
276 new_tokens_result.append((token_class, '', token_text))
276 new_tokens_result.append((token_class, '', token_text))
277 elif tag == 'delete':
277 elif tag == 'delete':
278 for token_class, token_text in old_tokens[o1:o2]:
278 for token_class, token_text in old_tokens[o1:o2]:
279 old_tokens_result.append((token_class, 'del', token_text))
279 old_tokens_result.append((token_class, 'del', token_text))
280 elif tag == 'insert':
280 elif tag == 'insert':
281 for token_class, token_text in new_tokens[n1:n2]:
281 for token_class, token_text in new_tokens[n1:n2]:
282 new_tokens_result.append((token_class, 'ins', token_text))
282 new_tokens_result.append((token_class, 'ins', token_text))
283 elif tag == 'replace':
283 elif tag == 'replace':
284 # if same type token blocks must be replaced, do a diff on the
284 # if same type token blocks must be replaced, do a diff on the
285 # characters in the token blocks to show individual changes
285 # characters in the token blocks to show individual changes
286
286
287 old_char_tokens = []
287 old_char_tokens = []
288 new_char_tokens = []
288 new_char_tokens = []
289 for token_class, token_text in old_tokens[o1:o2]:
289 for token_class, token_text in old_tokens[o1:o2]:
290 for char in token_text:
290 for char in token_text:
291 old_char_tokens.append((token_class, char))
291 old_char_tokens.append((token_class, char))
292
292
293 for token_class, token_text in new_tokens[n1:n2]:
293 for token_class, token_text in new_tokens[n1:n2]:
294 for char in token_text:
294 for char in token_text:
295 new_char_tokens.append((token_class, char))
295 new_char_tokens.append((token_class, char))
296
296
297 old_string = ''.join([token_text for
297 old_string = ''.join([token_text for
298 token_class, token_text in old_char_tokens])
298 token_class, token_text in old_char_tokens])
299 new_string = ''.join([token_text for
299 new_string = ''.join([token_text for
300 token_class, token_text in new_char_tokens])
300 token_class, token_text in new_char_tokens])
301
301
302 char_sequence = difflib.SequenceMatcher(
302 char_sequence = difflib.SequenceMatcher(
303 None, old_string, new_string)
303 None, old_string, new_string)
304 copcodes = char_sequence.get_opcodes()
304 copcodes = char_sequence.get_opcodes()
305 obuffer, nbuffer = [], []
305 obuffer, nbuffer = [], []
306
306
307 if use_diff_match_patch:
307 if use_diff_match_patch:
308 dmp = diff_match_patch()
308 dmp = diff_match_patch()
309 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
309 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
310 reps = dmp.diff_main(old_string, new_string)
310 reps = dmp.diff_main(old_string, new_string)
311 dmp.diff_cleanupEfficiency(reps)
311 dmp.diff_cleanupEfficiency(reps)
312
312
313 a, b = 0, 0
313 a, b = 0, 0
314 for op, rep in reps:
314 for op, rep in reps:
315 l = len(rep)
315 l = len(rep)
316 if op == 0:
316 if op == 0:
317 for i, c in enumerate(rep):
317 for i, c in enumerate(rep):
318 obuffer.append((old_char_tokens[a+i][0], '', c))
318 obuffer.append((old_char_tokens[a+i][0], '', c))
319 nbuffer.append((new_char_tokens[b+i][0], '', c))
319 nbuffer.append((new_char_tokens[b+i][0], '', c))
320 a += l
320 a += l
321 b += l
321 b += l
322 elif op == -1:
322 elif op == -1:
323 for i, c in enumerate(rep):
323 for i, c in enumerate(rep):
324 obuffer.append((old_char_tokens[a+i][0], 'del', c))
324 obuffer.append((old_char_tokens[a+i][0], 'del', c))
325 a += l
325 a += l
326 elif op == 1:
326 elif op == 1:
327 for i, c in enumerate(rep):
327 for i, c in enumerate(rep):
328 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
328 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
329 b += l
329 b += l
330 else:
330 else:
331 for ctag, co1, co2, cn1, cn2 in copcodes:
331 for ctag, co1, co2, cn1, cn2 in copcodes:
332 if ctag == 'equal':
332 if ctag == 'equal':
333 for token_class, token_text in old_char_tokens[co1:co2]:
333 for token_class, token_text in old_char_tokens[co1:co2]:
334 obuffer.append((token_class, '', token_text))
334 obuffer.append((token_class, '', token_text))
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 nbuffer.append((token_class, '', token_text))
336 nbuffer.append((token_class, '', token_text))
337 elif ctag == 'delete':
337 elif ctag == 'delete':
338 for token_class, token_text in old_char_tokens[co1:co2]:
338 for token_class, token_text in old_char_tokens[co1:co2]:
339 obuffer.append((token_class, 'del', token_text))
339 obuffer.append((token_class, 'del', token_text))
340 elif ctag == 'insert':
340 elif ctag == 'insert':
341 for token_class, token_text in new_char_tokens[cn1:cn2]:
341 for token_class, token_text in new_char_tokens[cn1:cn2]:
342 nbuffer.append((token_class, 'ins', token_text))
342 nbuffer.append((token_class, 'ins', token_text))
343 elif ctag == 'replace':
343 elif ctag == 'replace':
344 for token_class, token_text in old_char_tokens[co1:co2]:
344 for token_class, token_text in old_char_tokens[co1:co2]:
345 obuffer.append((token_class, 'del', token_text))
345 obuffer.append((token_class, 'del', token_text))
346 for token_class, token_text in new_char_tokens[cn1:cn2]:
346 for token_class, token_text in new_char_tokens[cn1:cn2]:
347 nbuffer.append((token_class, 'ins', token_text))
347 nbuffer.append((token_class, 'ins', token_text))
348
348
349 old_tokens_result.extend(obuffer)
349 old_tokens_result.extend(obuffer)
350 new_tokens_result.extend(nbuffer)
350 new_tokens_result.extend(nbuffer)
351
351
352 return old_tokens_result, new_tokens_result, similarity
352 return old_tokens_result, new_tokens_result, similarity
353
353
354
354
355 def diffset_node_getter(commit):
355 def diffset_node_getter(commit):
356 def get_node(fname):
356 def get_node(fname):
357 try:
357 try:
358 return commit.get_node(fname)
358 return commit.get_node(fname)
359 except NodeDoesNotExistError:
359 except NodeDoesNotExistError:
360 return None
360 return None
361
361
362 return get_node
362 return get_node
363
363
364
364
365 class DiffSet(object):
365 class DiffSet(object):
366 """
366 """
367 An object for parsing the diff result from diffs.DiffProcessor and
367 An object for parsing the diff result from diffs.DiffProcessor and
368 adding highlighting, side by side/unified renderings and line diffs
368 adding highlighting, side by side/unified renderings and line diffs
369 """
369 """
370
370
371 HL_REAL = 'REAL' # highlights using original file, slow
371 HL_REAL = 'REAL' # highlights using original file, slow
372 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
372 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
373 # in the case of multiline code
373 # in the case of multiline code
374 HL_NONE = 'NONE' # no highlighting, fastest
374 HL_NONE = 'NONE' # no highlighting, fastest
375
375
376 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
376 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
377 source_repo_name=None,
377 source_repo_name=None,
378 source_node_getter=lambda filename: None,
378 source_node_getter=lambda filename: None,
379 target_node_getter=lambda filename: None,
379 target_node_getter=lambda filename: None,
380 source_nodes=None, target_nodes=None,
380 source_nodes=None, target_nodes=None,
381 max_file_size_limit=150 * 1024, # files over this size will
381 max_file_size_limit=150 * 1024, # files over this size will
382 # use fast highlighting
382 # use fast highlighting
383 comments=None,
383 comments=None,
384 ):
384 ):
385
385
386 self.highlight_mode = highlight_mode
386 self.highlight_mode = highlight_mode
387 self.highlighted_filenodes = {}
387 self.highlighted_filenodes = {}
388 self.source_node_getter = source_node_getter
388 self.source_node_getter = source_node_getter
389 self.target_node_getter = target_node_getter
389 self.target_node_getter = target_node_getter
390 self.source_nodes = source_nodes or {}
390 self.source_nodes = source_nodes or {}
391 self.target_nodes = target_nodes or {}
391 self.target_nodes = target_nodes or {}
392 self.repo_name = repo_name
392 self.repo_name = repo_name
393 self.source_repo_name = source_repo_name or repo_name
393 self.source_repo_name = source_repo_name or repo_name
394 self.comments = comments or {}
394 self.comments = comments or {}
395 self.comments_store = self.comments.copy()
395 self.comments_store = self.comments.copy()
396 self.max_file_size_limit = max_file_size_limit
396 self.max_file_size_limit = max_file_size_limit
397
397
398 def render_patchset(self, patchset, source_ref=None, target_ref=None):
398 def render_patchset(self, patchset, source_ref=None, target_ref=None):
399 diffset = AttributeDict(dict(
399 diffset = AttributeDict(dict(
400 lines_added=0,
400 lines_added=0,
401 lines_deleted=0,
401 lines_deleted=0,
402 changed_files=0,
402 changed_files=0,
403 files=[],
403 files=[],
404 file_stats={},
404 file_stats={},
405 limited_diff=isinstance(patchset, LimitedDiffContainer),
405 limited_diff=isinstance(patchset, LimitedDiffContainer),
406 repo_name=self.repo_name,
406 repo_name=self.repo_name,
407 source_repo_name=self.source_repo_name,
407 source_repo_name=self.source_repo_name,
408 source_ref=source_ref,
408 source_ref=source_ref,
409 target_ref=target_ref,
409 target_ref=target_ref,
410 ))
410 ))
411 for patch in patchset:
411 for patch in patchset:
412 diffset.file_stats[patch['filename']] = patch['stats']
412 diffset.file_stats[patch['filename']] = patch['stats']
413 filediff = self.render_patch(patch)
413 filediff = self.render_patch(patch)
414 filediff.diffset = StrictAttributeDict(dict(
414 filediff.diffset = StrictAttributeDict(dict(
415 source_ref=diffset.source_ref,
415 source_ref=diffset.source_ref,
416 target_ref=diffset.target_ref,
416 target_ref=diffset.target_ref,
417 repo_name=diffset.repo_name,
417 repo_name=diffset.repo_name,
418 source_repo_name=diffset.source_repo_name,
418 source_repo_name=diffset.source_repo_name,
419 ))
419 ))
420 diffset.files.append(filediff)
420 diffset.files.append(filediff)
421 diffset.changed_files += 1
421 diffset.changed_files += 1
422 if not patch['stats']['binary']:
422 if not patch['stats']['binary']:
423 diffset.lines_added += patch['stats']['added']
423 diffset.lines_added += patch['stats']['added']
424 diffset.lines_deleted += patch['stats']['deleted']
424 diffset.lines_deleted += patch['stats']['deleted']
425
425
426 return diffset
426 return diffset
427
427
428 _lexer_cache = {}
428 _lexer_cache = {}
429
429
430 def _get_lexer_for_filename(self, filename, filenode=None):
430 def _get_lexer_for_filename(self, filename, filenode=None):
431 # cached because we might need to call it twice for source/target
431 # cached because we might need to call it twice for source/target
432 if filename not in self._lexer_cache:
432 if filename not in self._lexer_cache:
433 if filenode:
433 if filenode:
434 lexer = filenode.lexer
434 lexer = filenode.lexer
435 extension = filenode.extension
435 extension = filenode.extension
436 else:
436 else:
437 lexer = FileNode.get_lexer(filename=filename)
437 lexer = FileNode.get_lexer(filename=filename)
438 extension = filename.split('.')[-1]
438 extension = filename.split('.')[-1]
439
439
440 lexer = get_custom_lexer(extension) or lexer
440 lexer = get_custom_lexer(extension) or lexer
441 self._lexer_cache[filename] = lexer
441 self._lexer_cache[filename] = lexer
442 return self._lexer_cache[filename]
442 return self._lexer_cache[filename]
443
443
444 def render_patch(self, patch):
444 def render_patch(self, patch):
445 log.debug('rendering diff for %r' % patch['filename'])
445 log.debug('rendering diff for %r', patch['filename'])
446
446
447 source_filename = patch['original_filename']
447 source_filename = patch['original_filename']
448 target_filename = patch['filename']
448 target_filename = patch['filename']
449
449
450 source_lexer = plain_text_lexer
450 source_lexer = plain_text_lexer
451 target_lexer = plain_text_lexer
451 target_lexer = plain_text_lexer
452
452
453 if not patch['stats']['binary']:
453 if not patch['stats']['binary']:
454 if self.highlight_mode == self.HL_REAL:
454 if self.highlight_mode == self.HL_REAL:
455 if (source_filename and patch['operation'] in ('D', 'M')
455 if (source_filename and patch['operation'] in ('D', 'M')
456 and source_filename not in self.source_nodes):
456 and source_filename not in self.source_nodes):
457 self.source_nodes[source_filename] = (
457 self.source_nodes[source_filename] = (
458 self.source_node_getter(source_filename))
458 self.source_node_getter(source_filename))
459
459
460 if (target_filename and patch['operation'] in ('A', 'M')
460 if (target_filename and patch['operation'] in ('A', 'M')
461 and target_filename not in self.target_nodes):
461 and target_filename not in self.target_nodes):
462 self.target_nodes[target_filename] = (
462 self.target_nodes[target_filename] = (
463 self.target_node_getter(target_filename))
463 self.target_node_getter(target_filename))
464
464
465 elif self.highlight_mode == self.HL_FAST:
465 elif self.highlight_mode == self.HL_FAST:
466 source_lexer = self._get_lexer_for_filename(source_filename)
466 source_lexer = self._get_lexer_for_filename(source_filename)
467 target_lexer = self._get_lexer_for_filename(target_filename)
467 target_lexer = self._get_lexer_for_filename(target_filename)
468
468
469 source_file = self.source_nodes.get(source_filename, source_filename)
469 source_file = self.source_nodes.get(source_filename, source_filename)
470 target_file = self.target_nodes.get(target_filename, target_filename)
470 target_file = self.target_nodes.get(target_filename, target_filename)
471
471
472 source_filenode, target_filenode = None, None
472 source_filenode, target_filenode = None, None
473
473
474 # TODO: dan: FileNode.lexer works on the content of the file - which
474 # TODO: dan: FileNode.lexer works on the content of the file - which
475 # can be slow - issue #4289 explains a lexer clean up - which once
475 # can be slow - issue #4289 explains a lexer clean up - which once
476 # done can allow caching a lexer for a filenode to avoid the file lookup
476 # done can allow caching a lexer for a filenode to avoid the file lookup
477 if isinstance(source_file, FileNode):
477 if isinstance(source_file, FileNode):
478 source_filenode = source_file
478 source_filenode = source_file
479 #source_lexer = source_file.lexer
479 #source_lexer = source_file.lexer
480 source_lexer = self._get_lexer_for_filename(source_filename)
480 source_lexer = self._get_lexer_for_filename(source_filename)
481 source_file.lexer = source_lexer
481 source_file.lexer = source_lexer
482
482
483 if isinstance(target_file, FileNode):
483 if isinstance(target_file, FileNode):
484 target_filenode = target_file
484 target_filenode = target_file
485 #target_lexer = target_file.lexer
485 #target_lexer = target_file.lexer
486 target_lexer = self._get_lexer_for_filename(target_filename)
486 target_lexer = self._get_lexer_for_filename(target_filename)
487 target_file.lexer = target_lexer
487 target_file.lexer = target_lexer
488
488
489 source_file_path, target_file_path = None, None
489 source_file_path, target_file_path = None, None
490
490
491 if source_filename != '/dev/null':
491 if source_filename != '/dev/null':
492 source_file_path = source_filename
492 source_file_path = source_filename
493 if target_filename != '/dev/null':
493 if target_filename != '/dev/null':
494 target_file_path = target_filename
494 target_file_path = target_filename
495
495
496 source_file_type = source_lexer.name
496 source_file_type = source_lexer.name
497 target_file_type = target_lexer.name
497 target_file_type = target_lexer.name
498
498
499 filediff = AttributeDict({
499 filediff = AttributeDict({
500 'source_file_path': source_file_path,
500 'source_file_path': source_file_path,
501 'target_file_path': target_file_path,
501 'target_file_path': target_file_path,
502 'source_filenode': source_filenode,
502 'source_filenode': source_filenode,
503 'target_filenode': target_filenode,
503 'target_filenode': target_filenode,
504 'source_file_type': target_file_type,
504 'source_file_type': target_file_type,
505 'target_file_type': source_file_type,
505 'target_file_type': source_file_type,
506 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
506 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
507 'operation': patch['operation'],
507 'operation': patch['operation'],
508 'source_mode': patch['stats']['old_mode'],
508 'source_mode': patch['stats']['old_mode'],
509 'target_mode': patch['stats']['new_mode'],
509 'target_mode': patch['stats']['new_mode'],
510 'limited_diff': isinstance(patch, LimitedDiffContainer),
510 'limited_diff': isinstance(patch, LimitedDiffContainer),
511 'hunks': [],
511 'hunks': [],
512 'diffset': self,
512 'diffset': self,
513 })
513 })
514
514
515 for hunk in patch['chunks'][1:]:
515 for hunk in patch['chunks'][1:]:
516 hunkbit = self.parse_hunk(hunk, source_file, target_file)
516 hunkbit = self.parse_hunk(hunk, source_file, target_file)
517 hunkbit.source_file_path = source_file_path
517 hunkbit.source_file_path = source_file_path
518 hunkbit.target_file_path = target_file_path
518 hunkbit.target_file_path = target_file_path
519 filediff.hunks.append(hunkbit)
519 filediff.hunks.append(hunkbit)
520
520
521 left_comments = {}
521 left_comments = {}
522 if source_file_path in self.comments_store:
522 if source_file_path in self.comments_store:
523 for lineno, comments in self.comments_store[source_file_path].items():
523 for lineno, comments in self.comments_store[source_file_path].items():
524 left_comments[lineno] = comments
524 left_comments[lineno] = comments
525
525
526 if target_file_path in self.comments_store:
526 if target_file_path in self.comments_store:
527 for lineno, comments in self.comments_store[target_file_path].items():
527 for lineno, comments in self.comments_store[target_file_path].items():
528 left_comments[lineno] = comments
528 left_comments[lineno] = comments
529
529
530 # left comments are one that we couldn't place in diff lines.
530 # left comments are one that we couldn't place in diff lines.
531 # could be outdated, or the diff changed and this line is no
531 # could be outdated, or the diff changed and this line is no
532 # longer available
532 # longer available
533 filediff.left_comments = left_comments
533 filediff.left_comments = left_comments
534
534
535 return filediff
535 return filediff
536
536
537 def parse_hunk(self, hunk, source_file, target_file):
537 def parse_hunk(self, hunk, source_file, target_file):
538 result = AttributeDict(dict(
538 result = AttributeDict(dict(
539 source_start=hunk['source_start'],
539 source_start=hunk['source_start'],
540 source_length=hunk['source_length'],
540 source_length=hunk['source_length'],
541 target_start=hunk['target_start'],
541 target_start=hunk['target_start'],
542 target_length=hunk['target_length'],
542 target_length=hunk['target_length'],
543 section_header=hunk['section_header'],
543 section_header=hunk['section_header'],
544 lines=[],
544 lines=[],
545 ))
545 ))
546 before, after = [], []
546 before, after = [], []
547
547
548 for line in hunk['lines']:
548 for line in hunk['lines']:
549
549
550 if line['action'] == 'unmod':
550 if line['action'] == 'unmod':
551 result.lines.extend(
551 result.lines.extend(
552 self.parse_lines(before, after, source_file, target_file))
552 self.parse_lines(before, after, source_file, target_file))
553 after.append(line)
553 after.append(line)
554 before.append(line)
554 before.append(line)
555 elif line['action'] == 'add':
555 elif line['action'] == 'add':
556 after.append(line)
556 after.append(line)
557 elif line['action'] == 'del':
557 elif line['action'] == 'del':
558 before.append(line)
558 before.append(line)
559 elif line['action'] == 'old-no-nl':
559 elif line['action'] == 'old-no-nl':
560 before.append(line)
560 before.append(line)
561 elif line['action'] == 'new-no-nl':
561 elif line['action'] == 'new-no-nl':
562 after.append(line)
562 after.append(line)
563
563
564 result.lines.extend(
564 result.lines.extend(
565 self.parse_lines(before, after, source_file, target_file))
565 self.parse_lines(before, after, source_file, target_file))
566 result.unified = list(self.as_unified(result.lines))
566 result.unified = list(self.as_unified(result.lines))
567 result.sideside = result.lines
567 result.sideside = result.lines
568
568
569 return result
569 return result
570
570
571 def parse_lines(self, before_lines, after_lines, source_file, target_file):
571 def parse_lines(self, before_lines, after_lines, source_file, target_file):
572 # TODO: dan: investigate doing the diff comparison and fast highlighting
572 # TODO: dan: investigate doing the diff comparison and fast highlighting
573 # on the entire before and after buffered block lines rather than by
573 # on the entire before and after buffered block lines rather than by
574 # line, this means we can get better 'fast' highlighting if the context
574 # line, this means we can get better 'fast' highlighting if the context
575 # allows it - eg.
575 # allows it - eg.
576 # line 4: """
576 # line 4: """
577 # line 5: this gets highlighted as a string
577 # line 5: this gets highlighted as a string
578 # line 6: """
578 # line 6: """
579
579
580 lines = []
580 lines = []
581
581
582 before_newline = AttributeDict()
582 before_newline = AttributeDict()
583 after_newline = AttributeDict()
583 after_newline = AttributeDict()
584 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
584 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
585 before_newline_line = before_lines.pop(-1)
585 before_newline_line = before_lines.pop(-1)
586 before_newline.content = '\n {}'.format(
586 before_newline.content = '\n {}'.format(
587 render_tokenstream(
587 render_tokenstream(
588 [(x[0], '', x[1])
588 [(x[0], '', x[1])
589 for x in [('nonl', before_newline_line['line'])]]))
589 for x in [('nonl', before_newline_line['line'])]]))
590
590
591 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
591 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
592 after_newline_line = after_lines.pop(-1)
592 after_newline_line = after_lines.pop(-1)
593 after_newline.content = '\n {}'.format(
593 after_newline.content = '\n {}'.format(
594 render_tokenstream(
594 render_tokenstream(
595 [(x[0], '', x[1])
595 [(x[0], '', x[1])
596 for x in [('nonl', after_newline_line['line'])]]))
596 for x in [('nonl', after_newline_line['line'])]]))
597
597
598 while before_lines or after_lines:
598 while before_lines or after_lines:
599 before, after = None, None
599 before, after = None, None
600 before_tokens, after_tokens = None, None
600 before_tokens, after_tokens = None, None
601
601
602 if before_lines:
602 if before_lines:
603 before = before_lines.pop(0)
603 before = before_lines.pop(0)
604 if after_lines:
604 if after_lines:
605 after = after_lines.pop(0)
605 after = after_lines.pop(0)
606
606
607 original = AttributeDict()
607 original = AttributeDict()
608 modified = AttributeDict()
608 modified = AttributeDict()
609
609
610 if before:
610 if before:
611 if before['action'] == 'old-no-nl':
611 if before['action'] == 'old-no-nl':
612 before_tokens = [('nonl', before['line'])]
612 before_tokens = [('nonl', before['line'])]
613 else:
613 else:
614 before_tokens = self.get_line_tokens(
614 before_tokens = self.get_line_tokens(
615 line_text=before['line'],
615 line_text=before['line'],
616 line_number=before['old_lineno'],
616 line_number=before['old_lineno'],
617 file=source_file)
617 file=source_file)
618 original.lineno = before['old_lineno']
618 original.lineno = before['old_lineno']
619 original.content = before['line']
619 original.content = before['line']
620 original.action = self.action_to_op(before['action'])
620 original.action = self.action_to_op(before['action'])
621
621
622 original.get_comment_args = (
622 original.get_comment_args = (
623 source_file, 'o', before['old_lineno'])
623 source_file, 'o', before['old_lineno'])
624
624
625 if after:
625 if after:
626 if after['action'] == 'new-no-nl':
626 if after['action'] == 'new-no-nl':
627 after_tokens = [('nonl', after['line'])]
627 after_tokens = [('nonl', after['line'])]
628 else:
628 else:
629 after_tokens = self.get_line_tokens(
629 after_tokens = self.get_line_tokens(
630 line_text=after['line'], line_number=after['new_lineno'],
630 line_text=after['line'], line_number=after['new_lineno'],
631 file=target_file)
631 file=target_file)
632 modified.lineno = after['new_lineno']
632 modified.lineno = after['new_lineno']
633 modified.content = after['line']
633 modified.content = after['line']
634 modified.action = self.action_to_op(after['action'])
634 modified.action = self.action_to_op(after['action'])
635
635
636 modified.get_comment_args = (
636 modified.get_comment_args = (
637 target_file, 'n', after['new_lineno'])
637 target_file, 'n', after['new_lineno'])
638
638
639 # diff the lines
639 # diff the lines
640 if before_tokens and after_tokens:
640 if before_tokens and after_tokens:
641 o_tokens, m_tokens, similarity = tokens_diff(
641 o_tokens, m_tokens, similarity = tokens_diff(
642 before_tokens, after_tokens)
642 before_tokens, after_tokens)
643 original.content = render_tokenstream(o_tokens)
643 original.content = render_tokenstream(o_tokens)
644 modified.content = render_tokenstream(m_tokens)
644 modified.content = render_tokenstream(m_tokens)
645 elif before_tokens:
645 elif before_tokens:
646 original.content = render_tokenstream(
646 original.content = render_tokenstream(
647 [(x[0], '', x[1]) for x in before_tokens])
647 [(x[0], '', x[1]) for x in before_tokens])
648 elif after_tokens:
648 elif after_tokens:
649 modified.content = render_tokenstream(
649 modified.content = render_tokenstream(
650 [(x[0], '', x[1]) for x in after_tokens])
650 [(x[0], '', x[1]) for x in after_tokens])
651
651
652 if not before_lines and before_newline:
652 if not before_lines and before_newline:
653 original.content += before_newline.content
653 original.content += before_newline.content
654 before_newline = None
654 before_newline = None
655 if not after_lines and after_newline:
655 if not after_lines and after_newline:
656 modified.content += after_newline.content
656 modified.content += after_newline.content
657 after_newline = None
657 after_newline = None
658
658
659 lines.append(AttributeDict({
659 lines.append(AttributeDict({
660 'original': original,
660 'original': original,
661 'modified': modified,
661 'modified': modified,
662 }))
662 }))
663
663
664 return lines
664 return lines
665
665
666 def get_line_tokens(self, line_text, line_number, file=None):
666 def get_line_tokens(self, line_text, line_number, file=None):
667 filenode = None
667 filenode = None
668 filename = None
668 filename = None
669
669
670 if isinstance(file, basestring):
670 if isinstance(file, basestring):
671 filename = file
671 filename = file
672 elif isinstance(file, FileNode):
672 elif isinstance(file, FileNode):
673 filenode = file
673 filenode = file
674 filename = file.unicode_path
674 filename = file.unicode_path
675
675
676 if self.highlight_mode == self.HL_REAL and filenode:
676 if self.highlight_mode == self.HL_REAL and filenode:
677 lexer = self._get_lexer_for_filename(filename)
677 lexer = self._get_lexer_for_filename(filename)
678 file_size_allowed = file.size < self.max_file_size_limit
678 file_size_allowed = file.size < self.max_file_size_limit
679 if line_number and file_size_allowed:
679 if line_number and file_size_allowed:
680 return self.get_tokenized_filenode_line(
680 return self.get_tokenized_filenode_line(
681 file, line_number, lexer)
681 file, line_number, lexer)
682
682
683 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
683 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
684 lexer = self._get_lexer_for_filename(filename)
684 lexer = self._get_lexer_for_filename(filename)
685 return list(tokenize_string(line_text, lexer))
685 return list(tokenize_string(line_text, lexer))
686
686
687 return list(tokenize_string(line_text, plain_text_lexer))
687 return list(tokenize_string(line_text, plain_text_lexer))
688
688
689 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
689 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
690
690
691 if filenode not in self.highlighted_filenodes:
691 if filenode not in self.highlighted_filenodes:
692 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
692 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
693 self.highlighted_filenodes[filenode] = tokenized_lines
693 self.highlighted_filenodes[filenode] = tokenized_lines
694 return self.highlighted_filenodes[filenode][line_number - 1]
694 return self.highlighted_filenodes[filenode][line_number - 1]
695
695
696 def action_to_op(self, action):
696 def action_to_op(self, action):
697 return {
697 return {
698 'add': '+',
698 'add': '+',
699 'del': '-',
699 'del': '-',
700 'unmod': ' ',
700 'unmod': ' ',
701 'old-no-nl': ' ',
701 'old-no-nl': ' ',
702 'new-no-nl': ' ',
702 'new-no-nl': ' ',
703 }.get(action, action)
703 }.get(action, action)
704
704
705 def as_unified(self, lines):
705 def as_unified(self, lines):
706 """
706 """
707 Return a generator that yields the lines of a diff in unified order
707 Return a generator that yields the lines of a diff in unified order
708 """
708 """
709 def generator():
709 def generator():
710 buf = []
710 buf = []
711 for line in lines:
711 for line in lines:
712
712
713 if buf and not line.original or line.original.action == ' ':
713 if buf and not line.original or line.original.action == ' ':
714 for b in buf:
714 for b in buf:
715 yield b
715 yield b
716 buf = []
716 buf = []
717
717
718 if line.original:
718 if line.original:
719 if line.original.action == ' ':
719 if line.original.action == ' ':
720 yield (line.original.lineno, line.modified.lineno,
720 yield (line.original.lineno, line.modified.lineno,
721 line.original.action, line.original.content,
721 line.original.action, line.original.content,
722 line.original.get_comment_args)
722 line.original.get_comment_args)
723 continue
723 continue
724
724
725 if line.original.action == '-':
725 if line.original.action == '-':
726 yield (line.original.lineno, None,
726 yield (line.original.lineno, None,
727 line.original.action, line.original.content,
727 line.original.action, line.original.content,
728 line.original.get_comment_args)
728 line.original.get_comment_args)
729
729
730 if line.modified.action == '+':
730 if line.modified.action == '+':
731 buf.append((
731 buf.append((
732 None, line.modified.lineno,
732 None, line.modified.lineno,
733 line.modified.action, line.modified.content,
733 line.modified.action, line.modified.content,
734 line.modified.get_comment_args))
734 line.modified.get_comment_args))
735 continue
735 continue
736
736
737 if line.modified:
737 if line.modified:
738 yield (None, line.modified.lineno,
738 yield (None, line.modified.lineno,
739 line.modified.action, line.modified.content,
739 line.modified.action, line.modified.content,
740 line.modified.get_comment_args)
740 line.modified.get_comment_args)
741
741
742 for b in buf:
742 for b in buf:
743 yield b
743 yield b
744
744
745 return generator()
745 return generator()
@@ -1,621 +1,621 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 creation, and setup module for RhodeCode Enterprise. Used for creation
22 Database creation, and setup module for RhodeCode Enterprise. Used for creation
23 of database as well as for migration operations
23 of database as well as for migration operations
24 """
24 """
25
25
26 import os
26 import os
27 import sys
27 import sys
28 import time
28 import time
29 import uuid
29 import uuid
30 import logging
30 import logging
31 import getpass
31 import getpass
32 from os.path import dirname as dn, join as jn
32 from os.path import dirname as dn, join as jn
33
33
34 from sqlalchemy.engine import create_engine
34 from sqlalchemy.engine import create_engine
35
35
36 from rhodecode import __dbversion__
36 from rhodecode import __dbversion__
37 from rhodecode.model import init_model
37 from rhodecode.model import init_model
38 from rhodecode.model.user import UserModel
38 from rhodecode.model.user import UserModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
40 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
41 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
41 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
42 from rhodecode.model.meta import Session, Base
42 from rhodecode.model.meta import Session, Base
43 from rhodecode.model.permission import PermissionModel
43 from rhodecode.model.permission import PermissionModel
44 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.repo import RepoModel
45 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.repo_group import RepoGroupModel
46 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.settings import SettingsModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 def notify(msg):
52 def notify(msg):
53 """
53 """
54 Notification for migrations messages
54 Notification for migrations messages
55 """
55 """
56 ml = len(msg) + (4 * 2)
56 ml = len(msg) + (4 * 2)
57 print(('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper())
57 print(('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper())
58
58
59
59
60 class DbManage(object):
60 class DbManage(object):
61
61
62 def __init__(self, log_sql, dbconf, root, tests=False,
62 def __init__(self, log_sql, dbconf, root, tests=False,
63 SESSION=None, cli_args=None):
63 SESSION=None, cli_args=None):
64 self.dbname = dbconf.split('/')[-1]
64 self.dbname = dbconf.split('/')[-1]
65 self.tests = tests
65 self.tests = tests
66 self.root = root
66 self.root = root
67 self.dburi = dbconf
67 self.dburi = dbconf
68 self.log_sql = log_sql
68 self.log_sql = log_sql
69 self.db_exists = False
69 self.db_exists = False
70 self.cli_args = cli_args or {}
70 self.cli_args = cli_args or {}
71 self.init_db(SESSION=SESSION)
71 self.init_db(SESSION=SESSION)
72 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
72 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
73
73
74 def get_ask_ok_func(self, param):
74 def get_ask_ok_func(self, param):
75 if param not in [None]:
75 if param not in [None]:
76 # return a function lambda that has a default set to param
76 # return a function lambda that has a default set to param
77 return lambda *args, **kwargs: param
77 return lambda *args, **kwargs: param
78 else:
78 else:
79 from rhodecode.lib.utils import ask_ok
79 from rhodecode.lib.utils import ask_ok
80 return ask_ok
80 return ask_ok
81
81
82 def init_db(self, SESSION=None):
82 def init_db(self, SESSION=None):
83 if SESSION:
83 if SESSION:
84 self.sa = SESSION
84 self.sa = SESSION
85 else:
85 else:
86 # init new sessions
86 # init new sessions
87 engine = create_engine(self.dburi, echo=self.log_sql)
87 engine = create_engine(self.dburi, echo=self.log_sql)
88 init_model(engine)
88 init_model(engine)
89 self.sa = Session()
89 self.sa = Session()
90
90
91 def create_tables(self, override=False):
91 def create_tables(self, override=False):
92 """
92 """
93 Create a auth database
93 Create a auth database
94 """
94 """
95
95
96 log.info("Existing database with the same name is going to be destroyed.")
96 log.info("Existing database with the same name is going to be destroyed.")
97 log.info("Setup command will run DROP ALL command on that database.")
97 log.info("Setup command will run DROP ALL command on that database.")
98 if self.tests:
98 if self.tests:
99 destroy = True
99 destroy = True
100 else:
100 else:
101 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
101 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
102 if not destroy:
102 if not destroy:
103 log.info('Nothing done.')
103 log.info('Nothing done.')
104 sys.exit(0)
104 sys.exit(0)
105 if destroy:
105 if destroy:
106 Base.metadata.drop_all()
106 Base.metadata.drop_all()
107
107
108 checkfirst = not override
108 checkfirst = not override
109 Base.metadata.create_all(checkfirst=checkfirst)
109 Base.metadata.create_all(checkfirst=checkfirst)
110 log.info('Created tables for %s' % self.dbname)
110 log.info('Created tables for %s', self.dbname)
111
111
112 def set_db_version(self):
112 def set_db_version(self):
113 ver = DbMigrateVersion()
113 ver = DbMigrateVersion()
114 ver.version = __dbversion__
114 ver.version = __dbversion__
115 ver.repository_id = 'rhodecode_db_migrations'
115 ver.repository_id = 'rhodecode_db_migrations'
116 ver.repository_path = 'versions'
116 ver.repository_path = 'versions'
117 self.sa.add(ver)
117 self.sa.add(ver)
118 log.info('db version set to: %s' % __dbversion__)
118 log.info('db version set to: %s', __dbversion__)
119
119
120 def run_pre_migration_tasks(self):
120 def run_pre_migration_tasks(self):
121 """
121 """
122 Run various tasks before actually doing migrations
122 Run various tasks before actually doing migrations
123 """
123 """
124 # delete cache keys on each upgrade
124 # delete cache keys on each upgrade
125 total = CacheKey.query().count()
125 total = CacheKey.query().count()
126 log.info("Deleting (%s) cache keys now...", total)
126 log.info("Deleting (%s) cache keys now...", total)
127 CacheKey.delete_all_cache()
127 CacheKey.delete_all_cache()
128
128
129 def upgrade(self, version=None):
129 def upgrade(self, version=None):
130 """
130 """
131 Upgrades given database schema to given revision following
131 Upgrades given database schema to given revision following
132 all needed steps, to perform the upgrade
132 all needed steps, to perform the upgrade
133
133
134 """
134 """
135
135
136 from rhodecode.lib.dbmigrate.migrate.versioning import api
136 from rhodecode.lib.dbmigrate.migrate.versioning import api
137 from rhodecode.lib.dbmigrate.migrate.exceptions import \
137 from rhodecode.lib.dbmigrate.migrate.exceptions import \
138 DatabaseNotControlledError
138 DatabaseNotControlledError
139
139
140 if 'sqlite' in self.dburi:
140 if 'sqlite' in self.dburi:
141 print(
141 print(
142 '********************** WARNING **********************\n'
142 '********************** WARNING **********************\n'
143 'Make sure your version of sqlite is at least 3.7.X. \n'
143 'Make sure your version of sqlite is at least 3.7.X. \n'
144 'Earlier versions are known to fail on some migrations\n'
144 'Earlier versions are known to fail on some migrations\n'
145 '*****************************************************\n')
145 '*****************************************************\n')
146
146
147 upgrade = self.ask_ok(
147 upgrade = self.ask_ok(
148 'You are about to perform a database upgrade. Make '
148 'You are about to perform a database upgrade. Make '
149 'sure you have backed up your database. '
149 'sure you have backed up your database. '
150 'Continue ? [y/n]')
150 'Continue ? [y/n]')
151 if not upgrade:
151 if not upgrade:
152 log.info('No upgrade performed')
152 log.info('No upgrade performed')
153 sys.exit(0)
153 sys.exit(0)
154
154
155 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
155 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
156 'rhodecode/lib/dbmigrate')
156 'rhodecode/lib/dbmigrate')
157 db_uri = self.dburi
157 db_uri = self.dburi
158
158
159 try:
159 try:
160 curr_version = version or api.db_version(db_uri, repository_path)
160 curr_version = version or api.db_version(db_uri, repository_path)
161 msg = ('Found current database db_uri under version '
161 msg = ('Found current database db_uri under version '
162 'control with version {}'.format(curr_version))
162 'control with version {}'.format(curr_version))
163
163
164 except (RuntimeError, DatabaseNotControlledError):
164 except (RuntimeError, DatabaseNotControlledError):
165 curr_version = 1
165 curr_version = 1
166 msg = ('Current database is not under version control. Setting '
166 msg = ('Current database is not under version control. Setting '
167 'as version %s' % curr_version)
167 'as version %s' % curr_version)
168 api.version_control(db_uri, repository_path, curr_version)
168 api.version_control(db_uri, repository_path, curr_version)
169
169
170 notify(msg)
170 notify(msg)
171
171
172 self.run_pre_migration_tasks()
172 self.run_pre_migration_tasks()
173
173
174 if curr_version == __dbversion__:
174 if curr_version == __dbversion__:
175 log.info('This database is already at the newest version')
175 log.info('This database is already at the newest version')
176 sys.exit(0)
176 sys.exit(0)
177
177
178 upgrade_steps = range(curr_version + 1, __dbversion__ + 1)
178 upgrade_steps = range(curr_version + 1, __dbversion__ + 1)
179 notify('attempting to upgrade database from '
179 notify('attempting to upgrade database from '
180 'version %s to version %s' % (curr_version, __dbversion__))
180 'version %s to version %s' % (curr_version, __dbversion__))
181
181
182 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
182 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
183 _step = None
183 _step = None
184 for step in upgrade_steps:
184 for step in upgrade_steps:
185 notify('performing upgrade step %s' % step)
185 notify('performing upgrade step %s' % step)
186 time.sleep(0.5)
186 time.sleep(0.5)
187
187
188 api.upgrade(db_uri, repository_path, step)
188 api.upgrade(db_uri, repository_path, step)
189 self.sa.rollback()
189 self.sa.rollback()
190 notify('schema upgrade for step %s completed' % (step,))
190 notify('schema upgrade for step %s completed' % (step,))
191
191
192 _step = step
192 _step = step
193
193
194 notify('upgrade to version %s successful' % _step)
194 notify('upgrade to version %s successful' % _step)
195
195
196 def fix_repo_paths(self):
196 def fix_repo_paths(self):
197 """
197 """
198 Fixes an old RhodeCode version path into new one without a '*'
198 Fixes an old RhodeCode version path into new one without a '*'
199 """
199 """
200
200
201 paths = self.sa.query(RhodeCodeUi)\
201 paths = self.sa.query(RhodeCodeUi)\
202 .filter(RhodeCodeUi.ui_key == '/')\
202 .filter(RhodeCodeUi.ui_key == '/')\
203 .scalar()
203 .scalar()
204
204
205 paths.ui_value = paths.ui_value.replace('*', '')
205 paths.ui_value = paths.ui_value.replace('*', '')
206
206
207 try:
207 try:
208 self.sa.add(paths)
208 self.sa.add(paths)
209 self.sa.commit()
209 self.sa.commit()
210 except Exception:
210 except Exception:
211 self.sa.rollback()
211 self.sa.rollback()
212 raise
212 raise
213
213
214 def fix_default_user(self):
214 def fix_default_user(self):
215 """
215 """
216 Fixes an old default user with some 'nicer' default values,
216 Fixes an old default user with some 'nicer' default values,
217 used mostly for anonymous access
217 used mostly for anonymous access
218 """
218 """
219 def_user = self.sa.query(User)\
219 def_user = self.sa.query(User)\
220 .filter(User.username == User.DEFAULT_USER)\
220 .filter(User.username == User.DEFAULT_USER)\
221 .one()
221 .one()
222
222
223 def_user.name = 'Anonymous'
223 def_user.name = 'Anonymous'
224 def_user.lastname = 'User'
224 def_user.lastname = 'User'
225 def_user.email = User.DEFAULT_USER_EMAIL
225 def_user.email = User.DEFAULT_USER_EMAIL
226
226
227 try:
227 try:
228 self.sa.add(def_user)
228 self.sa.add(def_user)
229 self.sa.commit()
229 self.sa.commit()
230 except Exception:
230 except Exception:
231 self.sa.rollback()
231 self.sa.rollback()
232 raise
232 raise
233
233
234 def fix_settings(self):
234 def fix_settings(self):
235 """
235 """
236 Fixes rhodecode settings and adds ga_code key for google analytics
236 Fixes rhodecode settings and adds ga_code key for google analytics
237 """
237 """
238
238
239 hgsettings3 = RhodeCodeSetting('ga_code', '')
239 hgsettings3 = RhodeCodeSetting('ga_code', '')
240
240
241 try:
241 try:
242 self.sa.add(hgsettings3)
242 self.sa.add(hgsettings3)
243 self.sa.commit()
243 self.sa.commit()
244 except Exception:
244 except Exception:
245 self.sa.rollback()
245 self.sa.rollback()
246 raise
246 raise
247
247
248 def create_admin_and_prompt(self):
248 def create_admin_and_prompt(self):
249
249
250 # defaults
250 # defaults
251 defaults = self.cli_args
251 defaults = self.cli_args
252 username = defaults.get('username')
252 username = defaults.get('username')
253 password = defaults.get('password')
253 password = defaults.get('password')
254 email = defaults.get('email')
254 email = defaults.get('email')
255
255
256 if username is None:
256 if username is None:
257 username = raw_input('Specify admin username:')
257 username = raw_input('Specify admin username:')
258 if password is None:
258 if password is None:
259 password = self._get_admin_password()
259 password = self._get_admin_password()
260 if not password:
260 if not password:
261 # second try
261 # second try
262 password = self._get_admin_password()
262 password = self._get_admin_password()
263 if not password:
263 if not password:
264 sys.exit()
264 sys.exit()
265 if email is None:
265 if email is None:
266 email = raw_input('Specify admin email:')
266 email = raw_input('Specify admin email:')
267 api_key = self.cli_args.get('api_key')
267 api_key = self.cli_args.get('api_key')
268 self.create_user(username, password, email, True,
268 self.create_user(username, password, email, True,
269 strict_creation_check=False,
269 strict_creation_check=False,
270 api_key=api_key)
270 api_key=api_key)
271
271
272 def _get_admin_password(self):
272 def _get_admin_password(self):
273 password = getpass.getpass('Specify admin password '
273 password = getpass.getpass('Specify admin password '
274 '(min 6 chars):')
274 '(min 6 chars):')
275 confirm = getpass.getpass('Confirm password:')
275 confirm = getpass.getpass('Confirm password:')
276
276
277 if password != confirm:
277 if password != confirm:
278 log.error('passwords mismatch')
278 log.error('passwords mismatch')
279 return False
279 return False
280 if len(password) < 6:
280 if len(password) < 6:
281 log.error('password is too short - use at least 6 characters')
281 log.error('password is too short - use at least 6 characters')
282 return False
282 return False
283
283
284 return password
284 return password
285
285
286 def create_test_admin_and_users(self):
286 def create_test_admin_and_users(self):
287 log.info('creating admin and regular test users')
287 log.info('creating admin and regular test users')
288 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
288 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
289 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
289 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
290 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
290 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
291 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
291 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
292 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
292 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
293
293
294 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
294 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
295 TEST_USER_ADMIN_EMAIL, True, api_key=True)
295 TEST_USER_ADMIN_EMAIL, True, api_key=True)
296
296
297 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
297 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
298 TEST_USER_REGULAR_EMAIL, False, api_key=True)
298 TEST_USER_REGULAR_EMAIL, False, api_key=True)
299
299
300 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
300 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
301 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
301 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
302
302
303 def create_ui_settings(self, repo_store_path):
303 def create_ui_settings(self, repo_store_path):
304 """
304 """
305 Creates ui settings, fills out hooks
305 Creates ui settings, fills out hooks
306 and disables dotencode
306 and disables dotencode
307 """
307 """
308 settings_model = SettingsModel(sa=self.sa)
308 settings_model = SettingsModel(sa=self.sa)
309 from rhodecode.lib.vcs.backends.hg import largefiles_store
309 from rhodecode.lib.vcs.backends.hg import largefiles_store
310 from rhodecode.lib.vcs.backends.git import lfs_store
310 from rhodecode.lib.vcs.backends.git import lfs_store
311
311
312 # Build HOOKS
312 # Build HOOKS
313 hooks = [
313 hooks = [
314 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
314 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
315
315
316 # HG
316 # HG
317 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
317 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
318 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
318 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
319 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
319 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
320 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
320 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
321 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
321 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
322 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
322 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
323
323
324 ]
324 ]
325
325
326 for key, value in hooks:
326 for key, value in hooks:
327 hook_obj = settings_model.get_ui_by_key(key)
327 hook_obj = settings_model.get_ui_by_key(key)
328 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
328 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
329 hooks2.ui_section = 'hooks'
329 hooks2.ui_section = 'hooks'
330 hooks2.ui_key = key
330 hooks2.ui_key = key
331 hooks2.ui_value = value
331 hooks2.ui_value = value
332 self.sa.add(hooks2)
332 self.sa.add(hooks2)
333
333
334 # enable largefiles
334 # enable largefiles
335 largefiles = RhodeCodeUi()
335 largefiles = RhodeCodeUi()
336 largefiles.ui_section = 'extensions'
336 largefiles.ui_section = 'extensions'
337 largefiles.ui_key = 'largefiles'
337 largefiles.ui_key = 'largefiles'
338 largefiles.ui_value = ''
338 largefiles.ui_value = ''
339 self.sa.add(largefiles)
339 self.sa.add(largefiles)
340
340
341 # set default largefiles cache dir, defaults to
341 # set default largefiles cache dir, defaults to
342 # /repo_store_location/.cache/largefiles
342 # /repo_store_location/.cache/largefiles
343 largefiles = RhodeCodeUi()
343 largefiles = RhodeCodeUi()
344 largefiles.ui_section = 'largefiles'
344 largefiles.ui_section = 'largefiles'
345 largefiles.ui_key = 'usercache'
345 largefiles.ui_key = 'usercache'
346 largefiles.ui_value = largefiles_store(repo_store_path)
346 largefiles.ui_value = largefiles_store(repo_store_path)
347
347
348 self.sa.add(largefiles)
348 self.sa.add(largefiles)
349
349
350 # set default lfs cache dir, defaults to
350 # set default lfs cache dir, defaults to
351 # /repo_store_location/.cache/lfs_store
351 # /repo_store_location/.cache/lfs_store
352 lfsstore = RhodeCodeUi()
352 lfsstore = RhodeCodeUi()
353 lfsstore.ui_section = 'vcs_git_lfs'
353 lfsstore.ui_section = 'vcs_git_lfs'
354 lfsstore.ui_key = 'store_location'
354 lfsstore.ui_key = 'store_location'
355 lfsstore.ui_value = lfs_store(repo_store_path)
355 lfsstore.ui_value = lfs_store(repo_store_path)
356
356
357 self.sa.add(lfsstore)
357 self.sa.add(lfsstore)
358
358
359 # enable hgsubversion disabled by default
359 # enable hgsubversion disabled by default
360 hgsubversion = RhodeCodeUi()
360 hgsubversion = RhodeCodeUi()
361 hgsubversion.ui_section = 'extensions'
361 hgsubversion.ui_section = 'extensions'
362 hgsubversion.ui_key = 'hgsubversion'
362 hgsubversion.ui_key = 'hgsubversion'
363 hgsubversion.ui_value = ''
363 hgsubversion.ui_value = ''
364 hgsubversion.ui_active = False
364 hgsubversion.ui_active = False
365 self.sa.add(hgsubversion)
365 self.sa.add(hgsubversion)
366
366
367 # enable hgevolve disabled by default
367 # enable hgevolve disabled by default
368 hgevolve = RhodeCodeUi()
368 hgevolve = RhodeCodeUi()
369 hgevolve.ui_section = 'extensions'
369 hgevolve.ui_section = 'extensions'
370 hgevolve.ui_key = 'evolve'
370 hgevolve.ui_key = 'evolve'
371 hgevolve.ui_value = ''
371 hgevolve.ui_value = ''
372 hgevolve.ui_active = False
372 hgevolve.ui_active = False
373 self.sa.add(hgevolve)
373 self.sa.add(hgevolve)
374
374
375 # enable hggit disabled by default
375 # enable hggit disabled by default
376 hggit = RhodeCodeUi()
376 hggit = RhodeCodeUi()
377 hggit.ui_section = 'extensions'
377 hggit.ui_section = 'extensions'
378 hggit.ui_key = 'hggit'
378 hggit.ui_key = 'hggit'
379 hggit.ui_value = ''
379 hggit.ui_value = ''
380 hggit.ui_active = False
380 hggit.ui_active = False
381 self.sa.add(hggit)
381 self.sa.add(hggit)
382
382
383 # set svn branch defaults
383 # set svn branch defaults
384 branches = ["/branches/*", "/trunk"]
384 branches = ["/branches/*", "/trunk"]
385 tags = ["/tags/*"]
385 tags = ["/tags/*"]
386
386
387 for branch in branches:
387 for branch in branches:
388 settings_model.create_ui_section_value(
388 settings_model.create_ui_section_value(
389 RhodeCodeUi.SVN_BRANCH_ID, branch)
389 RhodeCodeUi.SVN_BRANCH_ID, branch)
390
390
391 for tag in tags:
391 for tag in tags:
392 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
392 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
393
393
394 def create_auth_plugin_options(self, skip_existing=False):
394 def create_auth_plugin_options(self, skip_existing=False):
395 """
395 """
396 Create default auth plugin settings, and make it active
396 Create default auth plugin settings, and make it active
397
397
398 :param skip_existing:
398 :param skip_existing:
399 """
399 """
400
400
401 for k, v, t in [('auth_plugins', 'egg:rhodecode-enterprise-ce#rhodecode', 'list'),
401 for k, v, t in [('auth_plugins', 'egg:rhodecode-enterprise-ce#rhodecode', 'list'),
402 ('auth_rhodecode_enabled', 'True', 'bool')]:
402 ('auth_rhodecode_enabled', 'True', 'bool')]:
403 if (skip_existing and
403 if (skip_existing and
404 SettingsModel().get_setting_by_name(k) is not None):
404 SettingsModel().get_setting_by_name(k) is not None):
405 log.debug('Skipping option %s' % k)
405 log.debug('Skipping option %s', k)
406 continue
406 continue
407 setting = RhodeCodeSetting(k, v, t)
407 setting = RhodeCodeSetting(k, v, t)
408 self.sa.add(setting)
408 self.sa.add(setting)
409
409
410 def create_default_options(self, skip_existing=False):
410 def create_default_options(self, skip_existing=False):
411 """Creates default settings"""
411 """Creates default settings"""
412
412
413 for k, v, t in [
413 for k, v, t in [
414 ('default_repo_enable_locking', False, 'bool'),
414 ('default_repo_enable_locking', False, 'bool'),
415 ('default_repo_enable_downloads', False, 'bool'),
415 ('default_repo_enable_downloads', False, 'bool'),
416 ('default_repo_enable_statistics', False, 'bool'),
416 ('default_repo_enable_statistics', False, 'bool'),
417 ('default_repo_private', False, 'bool'),
417 ('default_repo_private', False, 'bool'),
418 ('default_repo_type', 'hg', 'unicode')]:
418 ('default_repo_type', 'hg', 'unicode')]:
419
419
420 if (skip_existing and
420 if (skip_existing and
421 SettingsModel().get_setting_by_name(k) is not None):
421 SettingsModel().get_setting_by_name(k) is not None):
422 log.debug('Skipping option %s' % k)
422 log.debug('Skipping option %s', k)
423 continue
423 continue
424 setting = RhodeCodeSetting(k, v, t)
424 setting = RhodeCodeSetting(k, v, t)
425 self.sa.add(setting)
425 self.sa.add(setting)
426
426
427 def fixup_groups(self):
427 def fixup_groups(self):
428 def_usr = User.get_default_user()
428 def_usr = User.get_default_user()
429 for g in RepoGroup.query().all():
429 for g in RepoGroup.query().all():
430 g.group_name = g.get_new_name(g.name)
430 g.group_name = g.get_new_name(g.name)
431 self.sa.add(g)
431 self.sa.add(g)
432 # get default perm
432 # get default perm
433 default = UserRepoGroupToPerm.query()\
433 default = UserRepoGroupToPerm.query()\
434 .filter(UserRepoGroupToPerm.group == g)\
434 .filter(UserRepoGroupToPerm.group == g)\
435 .filter(UserRepoGroupToPerm.user == def_usr)\
435 .filter(UserRepoGroupToPerm.user == def_usr)\
436 .scalar()
436 .scalar()
437
437
438 if default is None:
438 if default is None:
439 log.debug('missing default permission for group %s adding' % g)
439 log.debug('missing default permission for group %s adding', g)
440 perm_obj = RepoGroupModel()._create_default_perms(g)
440 perm_obj = RepoGroupModel()._create_default_perms(g)
441 self.sa.add(perm_obj)
441 self.sa.add(perm_obj)
442
442
443 def reset_permissions(self, username):
443 def reset_permissions(self, username):
444 """
444 """
445 Resets permissions to default state, useful when old systems had
445 Resets permissions to default state, useful when old systems had
446 bad permissions, we must clean them up
446 bad permissions, we must clean them up
447
447
448 :param username:
448 :param username:
449 """
449 """
450 default_user = User.get_by_username(username)
450 default_user = User.get_by_username(username)
451 if not default_user:
451 if not default_user:
452 return
452 return
453
453
454 u2p = UserToPerm.query()\
454 u2p = UserToPerm.query()\
455 .filter(UserToPerm.user == default_user).all()
455 .filter(UserToPerm.user == default_user).all()
456 fixed = False
456 fixed = False
457 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
457 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
458 for p in u2p:
458 for p in u2p:
459 Session().delete(p)
459 Session().delete(p)
460 fixed = True
460 fixed = True
461 self.populate_default_permissions()
461 self.populate_default_permissions()
462 return fixed
462 return fixed
463
463
464 def update_repo_info(self):
464 def update_repo_info(self):
465 RepoModel.update_repoinfo()
465 RepoModel.update_repoinfo()
466
466
467 def config_prompt(self, test_repo_path='', retries=3):
467 def config_prompt(self, test_repo_path='', retries=3):
468 defaults = self.cli_args
468 defaults = self.cli_args
469 _path = defaults.get('repos_location')
469 _path = defaults.get('repos_location')
470 if retries == 3:
470 if retries == 3:
471 log.info('Setting up repositories config')
471 log.info('Setting up repositories config')
472
472
473 if _path is not None:
473 if _path is not None:
474 path = _path
474 path = _path
475 elif not self.tests and not test_repo_path:
475 elif not self.tests and not test_repo_path:
476 path = raw_input(
476 path = raw_input(
477 'Enter a valid absolute path to store repositories. '
477 'Enter a valid absolute path to store repositories. '
478 'All repositories in that path will be added automatically:'
478 'All repositories in that path will be added automatically:'
479 )
479 )
480 else:
480 else:
481 path = test_repo_path
481 path = test_repo_path
482 path_ok = True
482 path_ok = True
483
483
484 # check proper dir
484 # check proper dir
485 if not os.path.isdir(path):
485 if not os.path.isdir(path):
486 path_ok = False
486 path_ok = False
487 log.error('Given path %s is not a valid directory' % (path,))
487 log.error('Given path %s is not a valid directory', path)
488
488
489 elif not os.path.isabs(path):
489 elif not os.path.isabs(path):
490 path_ok = False
490 path_ok = False
491 log.error('Given path %s is not an absolute path' % (path,))
491 log.error('Given path %s is not an absolute path', path)
492
492
493 # check if path is at least readable.
493 # check if path is at least readable.
494 if not os.access(path, os.R_OK):
494 if not os.access(path, os.R_OK):
495 path_ok = False
495 path_ok = False
496 log.error('Given path %s is not readable' % (path,))
496 log.error('Given path %s is not readable', path)
497
497
498 # check write access, warn user about non writeable paths
498 # check write access, warn user about non writeable paths
499 elif not os.access(path, os.W_OK) and path_ok:
499 elif not os.access(path, os.W_OK) and path_ok:
500 log.warning('No write permission to given path %s' % (path,))
500 log.warning('No write permission to given path %s', path)
501
501
502 q = ('Given path %s is not writeable, do you want to '
502 q = ('Given path %s is not writeable, do you want to '
503 'continue with read only mode ? [y/n]' % (path,))
503 'continue with read only mode ? [y/n]' % (path,))
504 if not self.ask_ok(q):
504 if not self.ask_ok(q):
505 log.error('Canceled by user')
505 log.error('Canceled by user')
506 sys.exit(-1)
506 sys.exit(-1)
507
507
508 if retries == 0:
508 if retries == 0:
509 sys.exit('max retries reached')
509 sys.exit('max retries reached')
510 if not path_ok:
510 if not path_ok:
511 retries -= 1
511 retries -= 1
512 return self.config_prompt(test_repo_path, retries)
512 return self.config_prompt(test_repo_path, retries)
513
513
514 real_path = os.path.normpath(os.path.realpath(path))
514 real_path = os.path.normpath(os.path.realpath(path))
515
515
516 if real_path != os.path.normpath(path):
516 if real_path != os.path.normpath(path):
517 q = ('Path looks like a symlink, RhodeCode Enterprise will store '
517 q = ('Path looks like a symlink, RhodeCode Enterprise will store '
518 'given path as %s ? [y/n]') % (real_path,)
518 'given path as %s ? [y/n]') % (real_path,)
519 if not self.ask_ok(q):
519 if not self.ask_ok(q):
520 log.error('Canceled by user')
520 log.error('Canceled by user')
521 sys.exit(-1)
521 sys.exit(-1)
522
522
523 return real_path
523 return real_path
524
524
525 def create_settings(self, path):
525 def create_settings(self, path):
526
526
527 self.create_ui_settings(path)
527 self.create_ui_settings(path)
528
528
529 ui_config = [
529 ui_config = [
530 ('web', 'push_ssl', 'False'),
530 ('web', 'push_ssl', 'False'),
531 ('web', 'allow_archive', 'gz zip bz2'),
531 ('web', 'allow_archive', 'gz zip bz2'),
532 ('web', 'allow_push', '*'),
532 ('web', 'allow_push', '*'),
533 ('web', 'baseurl', '/'),
533 ('web', 'baseurl', '/'),
534 ('paths', '/', path),
534 ('paths', '/', path),
535 ('phases', 'publish', 'True')
535 ('phases', 'publish', 'True')
536 ]
536 ]
537 for section, key, value in ui_config:
537 for section, key, value in ui_config:
538 ui_conf = RhodeCodeUi()
538 ui_conf = RhodeCodeUi()
539 setattr(ui_conf, 'ui_section', section)
539 setattr(ui_conf, 'ui_section', section)
540 setattr(ui_conf, 'ui_key', key)
540 setattr(ui_conf, 'ui_key', key)
541 setattr(ui_conf, 'ui_value', value)
541 setattr(ui_conf, 'ui_value', value)
542 self.sa.add(ui_conf)
542 self.sa.add(ui_conf)
543
543
544 # rhodecode app settings
544 # rhodecode app settings
545 settings = [
545 settings = [
546 ('realm', 'RhodeCode', 'unicode'),
546 ('realm', 'RhodeCode', 'unicode'),
547 ('title', '', 'unicode'),
547 ('title', '', 'unicode'),
548 ('pre_code', '', 'unicode'),
548 ('pre_code', '', 'unicode'),
549 ('post_code', '', 'unicode'),
549 ('post_code', '', 'unicode'),
550 ('show_public_icon', True, 'bool'),
550 ('show_public_icon', True, 'bool'),
551 ('show_private_icon', True, 'bool'),
551 ('show_private_icon', True, 'bool'),
552 ('stylify_metatags', False, 'bool'),
552 ('stylify_metatags', False, 'bool'),
553 ('dashboard_items', 100, 'int'),
553 ('dashboard_items', 100, 'int'),
554 ('admin_grid_items', 25, 'int'),
554 ('admin_grid_items', 25, 'int'),
555 ('show_version', True, 'bool'),
555 ('show_version', True, 'bool'),
556 ('use_gravatar', False, 'bool'),
556 ('use_gravatar', False, 'bool'),
557 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
557 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
558 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
558 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
559 ('support_url', '', 'unicode'),
559 ('support_url', '', 'unicode'),
560 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
560 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
561 ('show_revision_number', True, 'bool'),
561 ('show_revision_number', True, 'bool'),
562 ('show_sha_length', 12, 'int'),
562 ('show_sha_length', 12, 'int'),
563 ]
563 ]
564
564
565 for key, val, type_ in settings:
565 for key, val, type_ in settings:
566 sett = RhodeCodeSetting(key, val, type_)
566 sett = RhodeCodeSetting(key, val, type_)
567 self.sa.add(sett)
567 self.sa.add(sett)
568
568
569 self.create_auth_plugin_options()
569 self.create_auth_plugin_options()
570 self.create_default_options()
570 self.create_default_options()
571
571
572 log.info('created ui config')
572 log.info('created ui config')
573
573
574 def create_user(self, username, password, email='', admin=False,
574 def create_user(self, username, password, email='', admin=False,
575 strict_creation_check=True, api_key=None):
575 strict_creation_check=True, api_key=None):
576 log.info('creating user `%s`' % username)
576 log.info('creating user `%s`', username)
577 user = UserModel().create_or_update(
577 user = UserModel().create_or_update(
578 username, password, email, firstname=u'RhodeCode', lastname=u'Admin',
578 username, password, email, firstname=u'RhodeCode', lastname=u'Admin',
579 active=True, admin=admin, extern_type="rhodecode",
579 active=True, admin=admin, extern_type="rhodecode",
580 strict_creation_check=strict_creation_check)
580 strict_creation_check=strict_creation_check)
581
581
582 if api_key:
582 if api_key:
583 log.info('setting a new default auth token for user `%s`', username)
583 log.info('setting a new default auth token for user `%s`', username)
584 UserModel().add_auth_token(
584 UserModel().add_auth_token(
585 user=user, lifetime_minutes=-1,
585 user=user, lifetime_minutes=-1,
586 role=UserModel.auth_token_role.ROLE_ALL,
586 role=UserModel.auth_token_role.ROLE_ALL,
587 description=u'BUILTIN TOKEN')
587 description=u'BUILTIN TOKEN')
588
588
589 def create_default_user(self):
589 def create_default_user(self):
590 log.info('creating default user')
590 log.info('creating default user')
591 # create default user for handling default permissions.
591 # create default user for handling default permissions.
592 user = UserModel().create_or_update(username=User.DEFAULT_USER,
592 user = UserModel().create_or_update(username=User.DEFAULT_USER,
593 password=str(uuid.uuid1())[:20],
593 password=str(uuid.uuid1())[:20],
594 email=User.DEFAULT_USER_EMAIL,
594 email=User.DEFAULT_USER_EMAIL,
595 firstname=u'Anonymous',
595 firstname=u'Anonymous',
596 lastname=u'User',
596 lastname=u'User',
597 strict_creation_check=False)
597 strict_creation_check=False)
598 # based on configuration options activate/de-activate this user which
598 # based on configuration options activate/de-activate this user which
599 # controlls anonymous access
599 # controlls anonymous access
600 if self.cli_args.get('public_access') is False:
600 if self.cli_args.get('public_access') is False:
601 log.info('Public access disabled')
601 log.info('Public access disabled')
602 user.active = False
602 user.active = False
603 Session().add(user)
603 Session().add(user)
604 Session().commit()
604 Session().commit()
605
605
606 def create_permissions(self):
606 def create_permissions(self):
607 """
607 """
608 Creates all permissions defined in the system
608 Creates all permissions defined in the system
609 """
609 """
610 # module.(access|create|change|delete)_[name]
610 # module.(access|create|change|delete)_[name]
611 # module.(none|read|write|admin)
611 # module.(none|read|write|admin)
612 log.info('creating permissions')
612 log.info('creating permissions')
613 PermissionModel(self.sa).create_permissions()
613 PermissionModel(self.sa).create_permissions()
614
614
615 def populate_default_permissions(self):
615 def populate_default_permissions(self):
616 """
616 """
617 Populate default permissions. It will create only the default
617 Populate default permissions. It will create only the default
618 permissions that are missing, and not alter already defined ones
618 permissions that are missing, and not alter already defined ones
619 """
619 """
620 log.info('creating default user permissions')
620 log.info('creating default user permissions')
621 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
621 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,100 +1,100 b''
1 """
1 """
2 Script to migrate repository from sqlalchemy <= 0.4.4 to the new
2 Script to migrate repository from sqlalchemy <= 0.4.4 to the new
3 repository schema. This shouldn't use any other migrate modules, so
3 repository schema. This shouldn't use any other migrate modules, so
4 that it can work in any version.
4 that it can work in any version.
5 """
5 """
6
6
7 import os
7 import os
8 import sys
8 import sys
9 import logging
9 import logging
10
10
11 log = logging.getLogger(__name__)
11 log = logging.getLogger(__name__)
12
12
13
13
14 def usage():
14 def usage():
15 """Gives usage information."""
15 """Gives usage information."""
16 print("""Usage: %(prog)s repository-to-migrate
16 print("""Usage: %(prog)s repository-to-migrate
17
17
18 Upgrade your repository to the new flat format.
18 Upgrade your repository to the new flat format.
19
19
20 NOTE: You should probably make a backup before running this.
20 NOTE: You should probably make a backup before running this.
21 """ % {'prog': sys.argv[0]})
21 """ % {'prog': sys.argv[0]})
22
22
23 sys.exit(1)
23 sys.exit(1)
24
24
25
25
26 def delete_file(filepath):
26 def delete_file(filepath):
27 """Deletes a file and prints a message."""
27 """Deletes a file and prints a message."""
28 log.info('Deleting file: %s' % filepath)
28 log.info('Deleting file: %s', filepath)
29 os.remove(filepath)
29 os.remove(filepath)
30
30
31
31
32 def move_file(src, tgt):
32 def move_file(src, tgt):
33 """Moves a file and prints a message."""
33 """Moves a file and prints a message."""
34 log.info('Moving file %s to %s' % (src, tgt))
34 log.info('Moving file %s to %s', src, tgt)
35 if os.path.exists(tgt):
35 if os.path.exists(tgt):
36 raise Exception(
36 raise Exception(
37 'Cannot move file %s because target %s already exists' % \
37 'Cannot move file %s because target %s already exists' % \
38 (src, tgt))
38 (src, tgt))
39 os.rename(src, tgt)
39 os.rename(src, tgt)
40
40
41
41
42 def delete_directory(dirpath):
42 def delete_directory(dirpath):
43 """Delete a directory and print a message."""
43 """Delete a directory and print a message."""
44 log.info('Deleting directory: %s' % dirpath)
44 log.info('Deleting directory: %s', dirpath)
45 os.rmdir(dirpath)
45 os.rmdir(dirpath)
46
46
47
47
48 def migrate_repository(repos):
48 def migrate_repository(repos):
49 """Does the actual migration to the new repository format."""
49 """Does the actual migration to the new repository format."""
50 log.info('Migrating repository at: %s to new format' % repos)
50 log.info('Migrating repository at: %s to new format', repos)
51 versions = '%s/versions' % repos
51 versions = '%s/versions' % repos
52 dirs = os.listdir(versions)
52 dirs = os.listdir(versions)
53 # Only use int's in list.
53 # Only use int's in list.
54 numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()]
54 numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()]
55 numdirs.sort() # Sort list.
55 numdirs.sort() # Sort list.
56 for dirname in numdirs:
56 for dirname in numdirs:
57 origdir = '%s/%s' % (versions, dirname)
57 origdir = '%s/%s' % (versions, dirname)
58 log.info('Working on directory: %s' % origdir)
58 log.info('Working on directory: %s', origdir)
59 files = os.listdir(origdir)
59 files = os.listdir(origdir)
60 files.sort()
60 files.sort()
61 for filename in files:
61 for filename in files:
62 # Delete compiled Python files.
62 # Delete compiled Python files.
63 if filename.endswith('.pyc') or filename.endswith('.pyo'):
63 if filename.endswith('.pyc') or filename.endswith('.pyo'):
64 delete_file('%s/%s' % (origdir, filename))
64 delete_file('%s/%s' % (origdir, filename))
65
65
66 # Delete empty __init__.py files.
66 # Delete empty __init__.py files.
67 origfile = '%s/__init__.py' % origdir
67 origfile = '%s/__init__.py' % origdir
68 if os.path.exists(origfile) and len(open(origfile).read()) == 0:
68 if os.path.exists(origfile) and len(open(origfile).read()) == 0:
69 delete_file(origfile)
69 delete_file(origfile)
70
70
71 # Move sql upgrade scripts.
71 # Move sql upgrade scripts.
72 if filename.endswith('.sql'):
72 if filename.endswith('.sql'):
73 version, dbms, operation = filename.split('.', 3)[0:3]
73 version, dbms, operation = filename.split('.', 3)[0:3]
74 origfile = '%s/%s' % (origdir, filename)
74 origfile = '%s/%s' % (origdir, filename)
75 # For instance: 2.postgres.upgrade.sql ->
75 # For instance: 2.postgres.upgrade.sql ->
76 # 002_postgres_upgrade.sql
76 # 002_postgres_upgrade.sql
77 tgtfile = '%s/%03d_%s_%s.sql' % (
77 tgtfile = '%s/%03d_%s_%s.sql' % (
78 versions, int(version), dbms, operation)
78 versions, int(version), dbms, operation)
79 move_file(origfile, tgtfile)
79 move_file(origfile, tgtfile)
80
80
81 # Move Python upgrade script.
81 # Move Python upgrade script.
82 pyfile = '%s.py' % dirname
82 pyfile = '%s.py' % dirname
83 pyfilepath = '%s/%s' % (origdir, pyfile)
83 pyfilepath = '%s/%s' % (origdir, pyfile)
84 if os.path.exists(pyfilepath):
84 if os.path.exists(pyfilepath):
85 tgtfile = '%s/%03d.py' % (versions, int(dirname))
85 tgtfile = '%s/%03d.py' % (versions, int(dirname))
86 move_file(pyfilepath, tgtfile)
86 move_file(pyfilepath, tgtfile)
87
87
88 # Try to remove directory. Will fail if it's not empty.
88 # Try to remove directory. Will fail if it's not empty.
89 delete_directory(origdir)
89 delete_directory(origdir)
90
90
91
91
92 def main():
92 def main():
93 """Main function to be called when using this script."""
93 """Main function to be called when using this script."""
94 if len(sys.argv) != 2:
94 if len(sys.argv) != 2:
95 usage()
95 usage()
96 migrate_repository(sys.argv[1])
96 migrate_repository(sys.argv[1])
97
97
98
98
99 if __name__ == '__main__':
99 if __name__ == '__main__':
100 main()
100 main()
@@ -1,75 +1,75 b''
1 """
1 """
2 A path/directory class.
2 A path/directory class.
3 """
3 """
4
4
5 import os
5 import os
6 import shutil
6 import shutil
7 import logging
7 import logging
8
8
9 from rhodecode.lib.dbmigrate.migrate import exceptions
9 from rhodecode.lib.dbmigrate.migrate import exceptions
10 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
10 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
11 from rhodecode.lib.dbmigrate.migrate.versioning.util import KeyedInstance
11 from rhodecode.lib.dbmigrate.migrate.versioning.util import KeyedInstance
12
12
13
13
14 log = logging.getLogger(__name__)
14 log = logging.getLogger(__name__)
15
15
16 class Pathed(KeyedInstance):
16 class Pathed(KeyedInstance):
17 """
17 """
18 A class associated with a path/directory tree.
18 A class associated with a path/directory tree.
19
19
20 Only one instance of this class may exist for a particular file;
20 Only one instance of this class may exist for a particular file;
21 __new__ will return an existing instance if possible
21 __new__ will return an existing instance if possible
22 """
22 """
23 parent = None
23 parent = None
24
24
25 @classmethod
25 @classmethod
26 def _key(cls, path):
26 def _key(cls, path):
27 return str(path)
27 return str(path)
28
28
29 def __init__(self, path):
29 def __init__(self, path):
30 self.path = path
30 self.path = path
31 if self.__class__.parent is not None:
31 if self.__class__.parent is not None:
32 self._init_parent(path)
32 self._init_parent(path)
33
33
34 def _init_parent(self, path):
34 def _init_parent(self, path):
35 """Try to initialize this object's parent, if it has one"""
35 """Try to initialize this object's parent, if it has one"""
36 parent_path = self.__class__._parent_path(path)
36 parent_path = self.__class__._parent_path(path)
37 self.parent = self.__class__.parent(parent_path)
37 self.parent = self.__class__.parent(parent_path)
38 log.debug("Getting parent %r:%r" % (self.__class__.parent, parent_path))
38 log.debug("Getting parent %r:%r", self.__class__.parent, parent_path)
39 self.parent._init_child(path, self)
39 self.parent._init_child(path, self)
40
40
41 def _init_child(self, child, path):
41 def _init_child(self, child, path):
42 """Run when a child of this object is initialized.
42 """Run when a child of this object is initialized.
43
43
44 Parameters: the child object; the path to this object (its
44 Parameters: the child object; the path to this object (its
45 parent)
45 parent)
46 """
46 """
47
47
48 @classmethod
48 @classmethod
49 def _parent_path(cls, path):
49 def _parent_path(cls, path):
50 """
50 """
51 Fetch the path of this object's parent from this object's path.
51 Fetch the path of this object's parent from this object's path.
52 """
52 """
53 # os.path.dirname(), but strip directories like files (like
53 # os.path.dirname(), but strip directories like files (like
54 # unix basename)
54 # unix basename)
55 #
55 #
56 # Treat directories like files...
56 # Treat directories like files...
57 if path[-1] == '/':
57 if path[-1] == '/':
58 path = path[:-1]
58 path = path[:-1]
59 ret = os.path.dirname(path)
59 ret = os.path.dirname(path)
60 return ret
60 return ret
61
61
62 @classmethod
62 @classmethod
63 def require_notfound(cls, path):
63 def require_notfound(cls, path):
64 """Ensures a given path does not already exist"""
64 """Ensures a given path does not already exist"""
65 if os.path.exists(path):
65 if os.path.exists(path):
66 raise exceptions.PathFoundError(path)
66 raise exceptions.PathFoundError(path)
67
67
68 @classmethod
68 @classmethod
69 def require_found(cls, path):
69 def require_found(cls, path):
70 """Ensures a given path already exists"""
70 """Ensures a given path already exists"""
71 if not os.path.exists(path):
71 if not os.path.exists(path):
72 raise exceptions.PathNotFoundError(path)
72 raise exceptions.PathNotFoundError(path)
73
73
74 def __str__(self):
74 def __str__(self):
75 return self.path
75 return self.path
@@ -1,243 +1,243 b''
1 """
1 """
2 SQLAlchemy migrate repository management.
2 SQLAlchemy migrate repository management.
3 """
3 """
4 import os
4 import os
5 import shutil
5 import shutil
6 import string
6 import string
7 import logging
7 import logging
8
8
9 from pkg_resources import resource_filename
9 from pkg_resources import resource_filename
10 from tempita import Template as TempitaTemplate
10 from tempita import Template as TempitaTemplate
11
11
12 from rhodecode.lib.dbmigrate.migrate import exceptions
12 from rhodecode.lib.dbmigrate.migrate import exceptions
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
16
16
17
17
18 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
19
19
20 class Changeset(dict):
20 class Changeset(dict):
21 """A collection of changes to be applied to a database.
21 """A collection of changes to be applied to a database.
22
22
23 Changesets are bound to a repository and manage a set of
23 Changesets are bound to a repository and manage a set of
24 scripts from that repository.
24 scripts from that repository.
25
25
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 """
27 """
28
28
29 def __init__(self, start, *changes, **k):
29 def __init__(self, start, *changes, **k):
30 """
30 """
31 Give a start version; step must be explicitly stated.
31 Give a start version; step must be explicitly stated.
32 """
32 """
33 self.step = k.pop('step', 1)
33 self.step = k.pop('step', 1)
34 self.start = version.VerNum(start)
34 self.start = version.VerNum(start)
35 self.end = self.start
35 self.end = self.start
36 for change in changes:
36 for change in changes:
37 self.add(change)
37 self.add(change)
38
38
39 def __iter__(self):
39 def __iter__(self):
40 return iter(self.items())
40 return iter(self.items())
41
41
42 def keys(self):
42 def keys(self):
43 """
43 """
44 In a series of upgrades x -> y, keys are version x. Sorted.
44 In a series of upgrades x -> y, keys are version x. Sorted.
45 """
45 """
46 ret = super(Changeset, self).keys()
46 ret = super(Changeset, self).keys()
47 # Reverse order if downgrading
47 # Reverse order if downgrading
48 ret.sort(reverse=(self.step < 1))
48 ret.sort(reverse=(self.step < 1))
49 return ret
49 return ret
50
50
51 def values(self):
51 def values(self):
52 return [self[k] for k in self.keys()]
52 return [self[k] for k in self.keys()]
53
53
54 def items(self):
54 def items(self):
55 return zip(self.keys(), self.values())
55 return zip(self.keys(), self.values())
56
56
57 def add(self, change):
57 def add(self, change):
58 """Add new change to changeset"""
58 """Add new change to changeset"""
59 key = self.end
59 key = self.end
60 self.end += self.step
60 self.end += self.step
61 self[key] = change
61 self[key] = change
62
62
63 def run(self, *p, **k):
63 def run(self, *p, **k):
64 """Run the changeset scripts"""
64 """Run the changeset scripts"""
65 for version, script in self:
65 for version, script in self:
66 script.run(*p, **k)
66 script.run(*p, **k)
67
67
68
68
69 class Repository(pathed.Pathed):
69 class Repository(pathed.Pathed):
70 """A project's change script repository"""
70 """A project's change script repository"""
71
71
72 _config = 'migrate.cfg'
72 _config = 'migrate.cfg'
73 _versions = 'versions'
73 _versions = 'versions'
74
74
75 def __init__(self, path):
75 def __init__(self, path):
76 log.debug('Loading repository %s...' % path)
76 log.debug('Loading repository %s...', path)
77 self.verify(path)
77 self.verify(path)
78 super(Repository, self).__init__(path)
78 super(Repository, self).__init__(path)
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 self.versions = version.Collection(os.path.join(self.path,
80 self.versions = version.Collection(os.path.join(self.path,
81 self._versions))
81 self._versions))
82 log.debug('Repository %s loaded successfully' % path)
82 log.debug('Repository %s loaded successfully', path)
83 log.debug('Config: %r' % self.config.to_dict())
83 log.debug('Config: %r', self.config.to_dict())
84
84
85 @classmethod
85 @classmethod
86 def verify(cls, path):
86 def verify(cls, path):
87 """
87 """
88 Ensure the target path is a valid repository.
88 Ensure the target path is a valid repository.
89
89
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 """
91 """
92 # Ensure the existence of required files
92 # Ensure the existence of required files
93 try:
93 try:
94 cls.require_found(path)
94 cls.require_found(path)
95 cls.require_found(os.path.join(path, cls._config))
95 cls.require_found(os.path.join(path, cls._config))
96 cls.require_found(os.path.join(path, cls._versions))
96 cls.require_found(os.path.join(path, cls._versions))
97 except exceptions.PathNotFoundError as e:
97 except exceptions.PathNotFoundError as e:
98 raise exceptions.InvalidRepositoryError(path)
98 raise exceptions.InvalidRepositoryError(path)
99
99
100 @classmethod
100 @classmethod
101 def prepare_config(cls, tmpl_dir, name, options=None):
101 def prepare_config(cls, tmpl_dir, name, options=None):
102 """
102 """
103 Prepare a project configuration file for a new project.
103 Prepare a project configuration file for a new project.
104
104
105 :param tmpl_dir: Path to Repository template
105 :param tmpl_dir: Path to Repository template
106 :param config_file: Name of the config file in Repository template
106 :param config_file: Name of the config file in Repository template
107 :param name: Repository name
107 :param name: Repository name
108 :type tmpl_dir: string
108 :type tmpl_dir: string
109 :type config_file: string
109 :type config_file: string
110 :type name: string
110 :type name: string
111 :returns: Populated config file
111 :returns: Populated config file
112 """
112 """
113 if options is None:
113 if options is None:
114 options = {}
114 options = {}
115 options.setdefault('version_table', 'migrate_version')
115 options.setdefault('version_table', 'migrate_version')
116 options.setdefault('repository_id', name)
116 options.setdefault('repository_id', name)
117 options.setdefault('required_dbs', [])
117 options.setdefault('required_dbs', [])
118 options.setdefault('use_timestamp_numbering', False)
118 options.setdefault('use_timestamp_numbering', False)
119
119
120 with open(os.path.join(tmpl_dir, cls._config)) as f:
120 with open(os.path.join(tmpl_dir, cls._config)) as f:
121 tmpl = f.read()
121 tmpl = f.read()
122 ret = TempitaTemplate(tmpl).substitute(options)
122 ret = TempitaTemplate(tmpl).substitute(options)
123
123
124 # cleanup
124 # cleanup
125 del options['__template_name__']
125 del options['__template_name__']
126
126
127 return ret
127 return ret
128
128
129 @classmethod
129 @classmethod
130 def create(cls, path, name, **opts):
130 def create(cls, path, name, **opts):
131 """Create a repository at a specified path"""
131 """Create a repository at a specified path"""
132 cls.require_notfound(path)
132 cls.require_notfound(path)
133 theme = opts.pop('templates_theme', None)
133 theme = opts.pop('templates_theme', None)
134 t_path = opts.pop('templates_path', None)
134 t_path = opts.pop('templates_path', None)
135
135
136 # Create repository
136 # Create repository
137 tmpl_dir = Template(t_path).get_repository(theme=theme)
137 tmpl_dir = Template(t_path).get_repository(theme=theme)
138 shutil.copytree(tmpl_dir, path)
138 shutil.copytree(tmpl_dir, path)
139
139
140 # Edit config defaults
140 # Edit config defaults
141 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
141 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
142 with open(os.path.join(path, cls._config), 'w') as fd:
142 with open(os.path.join(path, cls._config), 'w') as fd:
143 fd.write(config_text)
143 fd.write(config_text)
144
144
145 opts['repository_name'] = name
145 opts['repository_name'] = name
146
146
147 # Create a management script
147 # Create a management script
148 manager = os.path.join(path, 'manage.py')
148 manager = os.path.join(path, 'manage.py')
149 Repository.create_manage_file(manager, templates_theme=theme,
149 Repository.create_manage_file(manager, templates_theme=theme,
150 templates_path=t_path, **opts)
150 templates_path=t_path, **opts)
151
151
152 return cls(path)
152 return cls(path)
153
153
154 def create_script(self, description, **k):
154 def create_script(self, description, **k):
155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
156
156
157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
158 self.versions.create_new_python_version(description, **k)
158 self.versions.create_new_python_version(description, **k)
159
159
160 def create_script_sql(self, database, description, **k):
160 def create_script_sql(self, database, description, **k):
161 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
161 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
162 k['use_timestamp_numbering'] = self.use_timestamp_numbering
162 k['use_timestamp_numbering'] = self.use_timestamp_numbering
163 self.versions.create_new_sql_version(database, description, **k)
163 self.versions.create_new_sql_version(database, description, **k)
164
164
165 @property
165 @property
166 def latest(self):
166 def latest(self):
167 """API to :attr:`migrate.versioning.version.Collection.latest`"""
167 """API to :attr:`migrate.versioning.version.Collection.latest`"""
168 return self.versions.latest
168 return self.versions.latest
169
169
170 @property
170 @property
171 def version_table(self):
171 def version_table(self):
172 """Returns version_table name specified in config"""
172 """Returns version_table name specified in config"""
173 return self.config.get('db_settings', 'version_table')
173 return self.config.get('db_settings', 'version_table')
174
174
175 @property
175 @property
176 def id(self):
176 def id(self):
177 """Returns repository id specified in config"""
177 """Returns repository id specified in config"""
178 return self.config.get('db_settings', 'repository_id')
178 return self.config.get('db_settings', 'repository_id')
179
179
180 @property
180 @property
181 def use_timestamp_numbering(self):
181 def use_timestamp_numbering(self):
182 """Returns use_timestamp_numbering specified in config"""
182 """Returns use_timestamp_numbering specified in config"""
183 if self.config.has_option('db_settings', 'use_timestamp_numbering'):
183 if self.config.has_option('db_settings', 'use_timestamp_numbering'):
184 return self.config.getboolean('db_settings', 'use_timestamp_numbering')
184 return self.config.getboolean('db_settings', 'use_timestamp_numbering')
185 return False
185 return False
186
186
187 def version(self, *p, **k):
187 def version(self, *p, **k):
188 """API to :attr:`migrate.versioning.version.Collection.version`"""
188 """API to :attr:`migrate.versioning.version.Collection.version`"""
189 return self.versions.version(*p, **k)
189 return self.versions.version(*p, **k)
190
190
191 @classmethod
191 @classmethod
192 def clear(cls):
192 def clear(cls):
193 # TODO: deletes repo
193 # TODO: deletes repo
194 super(Repository, cls).clear()
194 super(Repository, cls).clear()
195 version.Collection.clear()
195 version.Collection.clear()
196
196
197 def changeset(self, database, start, end=None):
197 def changeset(self, database, start, end=None):
198 """Create a changeset to migrate this database from ver. start to end/latest.
198 """Create a changeset to migrate this database from ver. start to end/latest.
199
199
200 :param database: name of database to generate changeset
200 :param database: name of database to generate changeset
201 :param start: version to start at
201 :param start: version to start at
202 :param end: version to end at (latest if None given)
202 :param end: version to end at (latest if None given)
203 :type database: string
203 :type database: string
204 :type start: int
204 :type start: int
205 :type end: int
205 :type end: int
206 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
206 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
207 """
207 """
208 start = version.VerNum(start)
208 start = version.VerNum(start)
209
209
210 if end is None:
210 if end is None:
211 end = self.latest
211 end = self.latest
212 else:
212 else:
213 end = version.VerNum(end)
213 end = version.VerNum(end)
214
214
215 if start <= end:
215 if start <= end:
216 step = 1
216 step = 1
217 range_mod = 1
217 range_mod = 1
218 op = 'upgrade'
218 op = 'upgrade'
219 else:
219 else:
220 step = -1
220 step = -1
221 range_mod = 0
221 range_mod = 0
222 op = 'downgrade'
222 op = 'downgrade'
223
223
224 versions = range(int(start) + range_mod, int(end) + range_mod, step)
224 versions = range(int(start) + range_mod, int(end) + range_mod, step)
225 changes = [self.version(v).script(database, op) for v in versions]
225 changes = [self.version(v).script(database, op) for v in versions]
226 ret = Changeset(start, step=step, *changes)
226 ret = Changeset(start, step=step, *changes)
227 return ret
227 return ret
228
228
229 @classmethod
229 @classmethod
230 def create_manage_file(cls, file_, **opts):
230 def create_manage_file(cls, file_, **opts):
231 """Create a project management script (manage.py)
231 """Create a project management script (manage.py)
232
232
233 :param file_: Destination file to be written
233 :param file_: Destination file to be written
234 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
234 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
235 """
235 """
236 mng_file = Template(opts.pop('templates_path', None))\
236 mng_file = Template(opts.pop('templates_path', None))\
237 .get_manage(theme=opts.pop('templates_theme', None))
237 .get_manage(theme=opts.pop('templates_theme', None))
238
238
239 with open(mng_file) as f:
239 with open(mng_file) as f:
240 tmpl = f.read()
240 tmpl = f.read()
241
241
242 with open(file_, 'w') as fd:
242 with open(file_, 'w') as fd:
243 fd.write(TempitaTemplate(tmpl).substitute(opts))
243 fd.write(TempitaTemplate(tmpl).substitute(opts))
@@ -1,56 +1,56 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 import logging
3 import logging
4
4
5 from rhodecode.lib.dbmigrate.migrate import exceptions
5 from rhodecode.lib.dbmigrate.migrate import exceptions
6 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
6 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
7 from rhodecode.lib.dbmigrate.migrate.versioning import pathed
7 from rhodecode.lib.dbmigrate.migrate.versioning import pathed
8
8
9
9
10 log = logging.getLogger(__name__)
10 log = logging.getLogger(__name__)
11
11
12 class BaseScript(pathed.Pathed):
12 class BaseScript(pathed.Pathed):
13 """Base class for other types of scripts.
13 """Base class for other types of scripts.
14 All scripts have the following properties:
14 All scripts have the following properties:
15
15
16 source (script.source())
16 source (script.source())
17 The source code of the script
17 The source code of the script
18 version (script.version())
18 version (script.version())
19 The version number of the script
19 The version number of the script
20 operations (script.operations())
20 operations (script.operations())
21 The operations defined by the script: upgrade(), downgrade() or both.
21 The operations defined by the script: upgrade(), downgrade() or both.
22 Returns a tuple of operations.
22 Returns a tuple of operations.
23 Can also check for an operation with ex. script.operation(Script.ops.up)
23 Can also check for an operation with ex. script.operation(Script.ops.up)
24 """ # TODO: sphinxfy this and implement it correctly
24 """ # TODO: sphinxfy this and implement it correctly
25
25
26 def __init__(self, path):
26 def __init__(self, path):
27 log.debug('Loading script %s...' % path)
27 log.debug('Loading script %s...', path)
28 self.verify(path)
28 self.verify(path)
29 super(BaseScript, self).__init__(path)
29 super(BaseScript, self).__init__(path)
30 log.debug('Script %s loaded successfully' % path)
30 log.debug('Script %s loaded successfully', path)
31
31
32 @classmethod
32 @classmethod
33 def verify(cls, path):
33 def verify(cls, path):
34 """Ensure this is a valid script
34 """Ensure this is a valid script
35 This version simply ensures the script file's existence
35 This version simply ensures the script file's existence
36
36
37 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
37 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
38 """
38 """
39 try:
39 try:
40 cls.require_found(path)
40 cls.require_found(path)
41 except:
41 except:
42 raise exceptions.InvalidScriptError(path)
42 raise exceptions.InvalidScriptError(path)
43
43
44 def source(self):
44 def source(self):
45 """:returns: source code of the script.
45 """:returns: source code of the script.
46 :rtype: string
46 :rtype: string
47 """
47 """
48 with open(self.path) as fd:
48 with open(self.path) as fd:
49 ret = fd.read()
49 ret = fd.read()
50 return ret
50 return ret
51
51
52 def run(self, engine):
52 def run(self, engine):
53 """Core of each BaseScript subclass.
53 """Core of each BaseScript subclass.
54 This method executes the script.
54 This method executes the script.
55 """
55 """
56 raise NotImplementedError()
56 raise NotImplementedError()
@@ -1,1043 +1,1043 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 logging
22 import logging
23 import datetime
23 import datetime
24 import traceback
24 import traceback
25 from datetime import date
25 from datetime import date
26
26
27 from sqlalchemy import *
27 from sqlalchemy import *
28 from sqlalchemy.ext.hybrid import hybrid_property
28 from sqlalchemy.ext.hybrid import hybrid_property
29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
30 from beaker.cache import cache_region, region_invalidate
30 from beaker.cache import cache_region, region_invalidate
31
31
32 from rhodecode.lib.vcs import get_backend
32 from rhodecode.lib.vcs import get_backend
33 from rhodecode.lib.vcs.utils.helpers import get_scm
33 from rhodecode.lib.vcs.utils.helpers import get_scm
34 from rhodecode.lib.vcs.exceptions import VCSError
34 from rhodecode.lib.vcs.exceptions import VCSError
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 from rhodecode.lib.auth import generate_auth_token
36 from rhodecode.lib.auth import generate_auth_token
37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, safe_unicode
37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, safe_unicode
38 from rhodecode.lib.exceptions import UserGroupAssignedException
38 from rhodecode.lib.exceptions import UserGroupAssignedException
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40
40
41 from rhodecode.model.meta import Base, Session
41 from rhodecode.model.meta import Base, Session
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47 #==============================================================================
47 #==============================================================================
48 # BASE CLASSES
48 # BASE CLASSES
49 #==============================================================================
49 #==============================================================================
50
50
51 class ModelSerializer(json.JSONEncoder):
51 class ModelSerializer(json.JSONEncoder):
52 """
52 """
53 Simple Serializer for JSON,
53 Simple Serializer for JSON,
54
54
55 usage::
55 usage::
56
56
57 to make object customized for serialization implement a __json__
57 to make object customized for serialization implement a __json__
58 method that will return a dict for serialization into json
58 method that will return a dict for serialization into json
59
59
60 example::
60 example::
61
61
62 class Task(object):
62 class Task(object):
63
63
64 def __init__(self, name, value):
64 def __init__(self, name, value):
65 self.name = name
65 self.name = name
66 self.value = value
66 self.value = value
67
67
68 def __json__(self):
68 def __json__(self):
69 return dict(name=self.name,
69 return dict(name=self.name,
70 value=self.value)
70 value=self.value)
71
71
72 """
72 """
73
73
74 def default(self, obj):
74 def default(self, obj):
75
75
76 if hasattr(obj, '__json__'):
76 if hasattr(obj, '__json__'):
77 return obj.__json__()
77 return obj.__json__()
78 else:
78 else:
79 return json.JSONEncoder.default(self, obj)
79 return json.JSONEncoder.default(self, obj)
80
80
81 class BaseModel(object):
81 class BaseModel(object):
82 """Base Model for all classess
82 """Base Model for all classess
83
83
84 """
84 """
85
85
86 @classmethod
86 @classmethod
87 def _get_keys(cls):
87 def _get_keys(cls):
88 """return column names for this model """
88 """return column names for this model """
89 return class_mapper(cls).c.keys()
89 return class_mapper(cls).c.keys()
90
90
91 def get_dict(self):
91 def get_dict(self):
92 """return dict with keys and values corresponding
92 """return dict with keys and values corresponding
93 to this model data """
93 to this model data """
94
94
95 d = {}
95 d = {}
96 for k in self._get_keys():
96 for k in self._get_keys():
97 d[k] = getattr(self, k)
97 d[k] = getattr(self, k)
98 return d
98 return d
99
99
100 def get_appstruct(self):
100 def get_appstruct(self):
101 """return list with keys and values tupples corresponding
101 """return list with keys and values tupples corresponding
102 to this model data """
102 to this model data """
103
103
104 l = []
104 l = []
105 for k in self._get_keys():
105 for k in self._get_keys():
106 l.append((k, getattr(self, k),))
106 l.append((k, getattr(self, k),))
107 return l
107 return l
108
108
109 def populate_obj(self, populate_dict):
109 def populate_obj(self, populate_dict):
110 """populate model with data from given populate_dict"""
110 """populate model with data from given populate_dict"""
111
111
112 for k in self._get_keys():
112 for k in self._get_keys():
113 if k in populate_dict:
113 if k in populate_dict:
114 setattr(self, k, populate_dict[k])
114 setattr(self, k, populate_dict[k])
115
115
116 @classmethod
116 @classmethod
117 def query(cls):
117 def query(cls):
118 return Session.query(cls)
118 return Session.query(cls)
119
119
120 @classmethod
120 @classmethod
121 def get(cls, id_):
121 def get(cls, id_):
122 if id_:
122 if id_:
123 return cls.query().get(id_)
123 return cls.query().get(id_)
124
124
125 @classmethod
125 @classmethod
126 def getAll(cls):
126 def getAll(cls):
127 return cls.query().all()
127 return cls.query().all()
128
128
129 @classmethod
129 @classmethod
130 def delete(cls, id_):
130 def delete(cls, id_):
131 obj = cls.query().get(id_)
131 obj = cls.query().get(id_)
132 Session.delete(obj)
132 Session.delete(obj)
133 Session.commit()
133 Session.commit()
134
134
135
135
136 class RhodeCodeSetting(Base, BaseModel):
136 class RhodeCodeSetting(Base, BaseModel):
137 __tablename__ = 'rhodecode_settings'
137 __tablename__ = 'rhodecode_settings'
138 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
138 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
139 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
139 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
140 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
140 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
141 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
141 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
142
142
143 def __init__(self, k='', v=''):
143 def __init__(self, k='', v=''):
144 self.app_settings_name = k
144 self.app_settings_name = k
145 self.app_settings_value = v
145 self.app_settings_value = v
146
146
147
147
148 @validates('_app_settings_value')
148 @validates('_app_settings_value')
149 def validate_settings_value(self, key, val):
149 def validate_settings_value(self, key, val):
150 assert type(val) == unicode
150 assert type(val) == unicode
151 return val
151 return val
152
152
153 @hybrid_property
153 @hybrid_property
154 def app_settings_value(self):
154 def app_settings_value(self):
155 v = self._app_settings_value
155 v = self._app_settings_value
156 if v == 'ldap_active':
156 if v == 'ldap_active':
157 v = str2bool(v)
157 v = str2bool(v)
158 return v
158 return v
159
159
160 @app_settings_value.setter
160 @app_settings_value.setter
161 def app_settings_value(self, val):
161 def app_settings_value(self, val):
162 """
162 """
163 Setter that will always make sure we use unicode in app_settings_value
163 Setter that will always make sure we use unicode in app_settings_value
164
164
165 :param val:
165 :param val:
166 """
166 """
167 self._app_settings_value = safe_unicode(val)
167 self._app_settings_value = safe_unicode(val)
168
168
169 def __repr__(self):
169 def __repr__(self):
170 return "<%s('%s:%s')>" % (self.__class__.__name__,
170 return "<%s('%s:%s')>" % (self.__class__.__name__,
171 self.app_settings_name, self.app_settings_value)
171 self.app_settings_name, self.app_settings_value)
172
172
173
173
174 @classmethod
174 @classmethod
175 def get_by_name(cls, ldap_key):
175 def get_by_name(cls, ldap_key):
176 return cls.query()\
176 return cls.query()\
177 .filter(cls.app_settings_name == ldap_key).scalar()
177 .filter(cls.app_settings_name == ldap_key).scalar()
178
178
179 @classmethod
179 @classmethod
180 def get_app_settings(cls, cache=False):
180 def get_app_settings(cls, cache=False):
181
181
182 ret = cls.query()
182 ret = cls.query()
183
183
184 if cache:
184 if cache:
185 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
185 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
186
186
187 if not ret:
187 if not ret:
188 raise Exception('Could not get application settings !')
188 raise Exception('Could not get application settings !')
189 settings = {}
189 settings = {}
190 for each in ret:
190 for each in ret:
191 settings['rhodecode_' + each.app_settings_name] = \
191 settings['rhodecode_' + each.app_settings_name] = \
192 each.app_settings_value
192 each.app_settings_value
193
193
194 return settings
194 return settings
195
195
196 @classmethod
196 @classmethod
197 def get_ldap_settings(cls, cache=False):
197 def get_ldap_settings(cls, cache=False):
198 ret = cls.query()\
198 ret = cls.query()\
199 .filter(cls.app_settings_name.startswith('ldap_')).all()
199 .filter(cls.app_settings_name.startswith('ldap_')).all()
200 fd = {}
200 fd = {}
201 for row in ret:
201 for row in ret:
202 fd.update({row.app_settings_name:row.app_settings_value})
202 fd.update({row.app_settings_name:row.app_settings_value})
203
203
204 return fd
204 return fd
205
205
206
206
207 class RhodeCodeUi(Base, BaseModel):
207 class RhodeCodeUi(Base, BaseModel):
208 __tablename__ = 'rhodecode_ui'
208 __tablename__ = 'rhodecode_ui'
209 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
209 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
210
210
211 HOOK_REPO_SIZE = 'changegroup.repo_size'
211 HOOK_REPO_SIZE = 'changegroup.repo_size'
212 HOOK_PUSH = 'pretxnchangegroup.push_logger'
212 HOOK_PUSH = 'pretxnchangegroup.push_logger'
213 HOOK_PULL = 'preoutgoing.pull_logger'
213 HOOK_PULL = 'preoutgoing.pull_logger'
214
214
215 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
215 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
216 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
216 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
217 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
217 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
218 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
218 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
219 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
219 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
220
220
221
221
222 @classmethod
222 @classmethod
223 def get_by_key(cls, key):
223 def get_by_key(cls, key):
224 return cls.query().filter(cls.ui_key == key)
224 return cls.query().filter(cls.ui_key == key)
225
225
226
226
227 @classmethod
227 @classmethod
228 def get_builtin_hooks(cls):
228 def get_builtin_hooks(cls):
229 q = cls.query()
229 q = cls.query()
230 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
230 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
231 cls.HOOK_PUSH, cls.HOOK_PULL]))
231 cls.HOOK_PUSH, cls.HOOK_PULL]))
232 return q.all()
232 return q.all()
233
233
234 @classmethod
234 @classmethod
235 def get_custom_hooks(cls):
235 def get_custom_hooks(cls):
236 q = cls.query()
236 q = cls.query()
237 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
237 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
238 cls.HOOK_PUSH, cls.HOOK_PULL]))
238 cls.HOOK_PUSH, cls.HOOK_PULL]))
239 q = q.filter(cls.ui_section == 'hooks')
239 q = q.filter(cls.ui_section == 'hooks')
240 return q.all()
240 return q.all()
241
241
242 @classmethod
242 @classmethod
243 def create_or_update_hook(cls, key, val):
243 def create_or_update_hook(cls, key, val):
244 new_ui = cls.get_by_key(key).scalar() or cls()
244 new_ui = cls.get_by_key(key).scalar() or cls()
245 new_ui.ui_section = 'hooks'
245 new_ui.ui_section = 'hooks'
246 new_ui.ui_active = True
246 new_ui.ui_active = True
247 new_ui.ui_key = key
247 new_ui.ui_key = key
248 new_ui.ui_value = val
248 new_ui.ui_value = val
249
249
250 Session.add(new_ui)
250 Session.add(new_ui)
251 Session.commit()
251 Session.commit()
252
252
253
253
254 class User(Base, BaseModel):
254 class User(Base, BaseModel):
255 __tablename__ = 'users'
255 __tablename__ = 'users'
256 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
256 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
257 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
257 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
258 username = Column("username", String(255), nullable=True, unique=None, default=None)
258 username = Column("username", String(255), nullable=True, unique=None, default=None)
259 password = Column("password", String(255), nullable=True, unique=None, default=None)
259 password = Column("password", String(255), nullable=True, unique=None, default=None)
260 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
260 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
261 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
261 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
262 name = Column("name", String(255), nullable=True, unique=None, default=None)
262 name = Column("name", String(255), nullable=True, unique=None, default=None)
263 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
263 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
264 email = Column("email", String(255), nullable=True, unique=None, default=None)
264 email = Column("email", String(255), nullable=True, unique=None, default=None)
265 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
265 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
266 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
266 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
267 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
267 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
268
268
269 user_log = relationship('UserLog', cascade='all')
269 user_log = relationship('UserLog', cascade='all')
270 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
270 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
271
271
272 repositories = relationship('Repository')
272 repositories = relationship('Repository')
273 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
273 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
274 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
274 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
275
275
276 group_member = relationship('UserGroupMember', cascade='all')
276 group_member = relationship('UserGroupMember', cascade='all')
277
277
278 @property
278 @property
279 def full_contact(self):
279 def full_contact(self):
280 return '%s %s <%s>' % (self.name, self.lastname, self.email)
280 return '%s %s <%s>' % (self.name, self.lastname, self.email)
281
281
282 @property
282 @property
283 def short_contact(self):
283 def short_contact(self):
284 return '%s %s' % (self.name, self.lastname)
284 return '%s %s' % (self.name, self.lastname)
285
285
286 @property
286 @property
287 def is_admin(self):
287 def is_admin(self):
288 return self.admin
288 return self.admin
289
289
290 def __repr__(self):
290 def __repr__(self):
291 try:
291 try:
292 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
292 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
293 self.user_id, self.username)
293 self.user_id, self.username)
294 except:
294 except:
295 return self.__class__.__name__
295 return self.__class__.__name__
296
296
297 @classmethod
297 @classmethod
298 def get_by_username(cls, username, case_insensitive=False):
298 def get_by_username(cls, username, case_insensitive=False):
299 if case_insensitive:
299 if case_insensitive:
300 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
300 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
301 else:
301 else:
302 return Session.query(cls).filter(cls.username == username).scalar()
302 return Session.query(cls).filter(cls.username == username).scalar()
303
303
304 @classmethod
304 @classmethod
305 def get_by_auth_token(cls, auth_token):
305 def get_by_auth_token(cls, auth_token):
306 return cls.query().filter(cls.api_key == auth_token).one()
306 return cls.query().filter(cls.api_key == auth_token).one()
307
307
308 def update_lastlogin(self):
308 def update_lastlogin(self):
309 """Update user lastlogin"""
309 """Update user lastlogin"""
310
310
311 self.last_login = datetime.datetime.now()
311 self.last_login = datetime.datetime.now()
312 Session.add(self)
312 Session.add(self)
313 Session.commit()
313 Session.commit()
314 log.debug('updated user %s lastlogin' % self.username)
314 log.debug('updated user %s lastlogin', self.username)
315
315
316 @classmethod
316 @classmethod
317 def create(cls, form_data):
317 def create(cls, form_data):
318 from rhodecode.lib.auth import get_crypt_password
318 from rhodecode.lib.auth import get_crypt_password
319
319
320 try:
320 try:
321 new_user = cls()
321 new_user = cls()
322 for k, v in form_data.items():
322 for k, v in form_data.items():
323 if k == 'password':
323 if k == 'password':
324 v = get_crypt_password(v)
324 v = get_crypt_password(v)
325 setattr(new_user, k, v)
325 setattr(new_user, k, v)
326
326
327 new_user.api_key = generate_auth_token(form_data['username'])
327 new_user.api_key = generate_auth_token(form_data['username'])
328 Session.add(new_user)
328 Session.add(new_user)
329 Session.commit()
329 Session.commit()
330 return new_user
330 return new_user
331 except:
331 except:
332 log.error(traceback.format_exc())
332 log.error(traceback.format_exc())
333 Session.rollback()
333 Session.rollback()
334 raise
334 raise
335
335
336 class UserLog(Base, BaseModel):
336 class UserLog(Base, BaseModel):
337 __tablename__ = 'user_logs'
337 __tablename__ = 'user_logs'
338 __table_args__ = {'extend_existing':True}
338 __table_args__ = {'extend_existing':True}
339 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
339 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
340 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
340 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
341 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
341 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
342 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
342 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
343 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
343 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
344 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
344 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
345 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
345 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
346
346
347 @property
347 @property
348 def action_as_day(self):
348 def action_as_day(self):
349 return date(*self.action_date.timetuple()[:3])
349 return date(*self.action_date.timetuple()[:3])
350
350
351 user = relationship('User')
351 user = relationship('User')
352 repository = relationship('Repository')
352 repository = relationship('Repository')
353
353
354
354
355 class UserGroup(Base, BaseModel):
355 class UserGroup(Base, BaseModel):
356 __tablename__ = 'users_groups'
356 __tablename__ = 'users_groups'
357 __table_args__ = {'extend_existing':True}
357 __table_args__ = {'extend_existing':True}
358
358
359 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
359 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
360 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
360 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
361 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
361 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
362
362
363 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
363 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
364
364
365 def __repr__(self):
365 def __repr__(self):
366 return '<userGroup(%s)>' % (self.users_group_name)
366 return '<userGroup(%s)>' % (self.users_group_name)
367
367
368 @classmethod
368 @classmethod
369 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
369 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
370 if case_insensitive:
370 if case_insensitive:
371 gr = cls.query()\
371 gr = cls.query()\
372 .filter(cls.users_group_name.ilike(group_name))
372 .filter(cls.users_group_name.ilike(group_name))
373 else:
373 else:
374 gr = cls.query()\
374 gr = cls.query()\
375 .filter(cls.users_group_name == group_name)
375 .filter(cls.users_group_name == group_name)
376 if cache:
376 if cache:
377 gr = gr.options(FromCache("sql_cache_short",
377 gr = gr.options(FromCache("sql_cache_short",
378 "get_user_%s" % group_name))
378 "get_user_%s" % group_name))
379 return gr.scalar()
379 return gr.scalar()
380
380
381 @classmethod
381 @classmethod
382 def get(cls, users_group_id, cache=False):
382 def get(cls, users_group_id, cache=False):
383 users_group = cls.query()
383 users_group = cls.query()
384 if cache:
384 if cache:
385 users_group = users_group.options(FromCache("sql_cache_short",
385 users_group = users_group.options(FromCache("sql_cache_short",
386 "get_users_group_%s" % users_group_id))
386 "get_users_group_%s" % users_group_id))
387 return users_group.get(users_group_id)
387 return users_group.get(users_group_id)
388
388
389 @classmethod
389 @classmethod
390 def create(cls, form_data):
390 def create(cls, form_data):
391 try:
391 try:
392 new_user_group = cls()
392 new_user_group = cls()
393 for k, v in form_data.items():
393 for k, v in form_data.items():
394 setattr(new_user_group, k, v)
394 setattr(new_user_group, k, v)
395
395
396 Session.add(new_user_group)
396 Session.add(new_user_group)
397 Session.commit()
397 Session.commit()
398 return new_user_group
398 return new_user_group
399 except:
399 except:
400 log.error(traceback.format_exc())
400 log.error(traceback.format_exc())
401 Session.rollback()
401 Session.rollback()
402 raise
402 raise
403
403
404 @classmethod
404 @classmethod
405 def update(cls, users_group_id, form_data):
405 def update(cls, users_group_id, form_data):
406
406
407 try:
407 try:
408 users_group = cls.get(users_group_id, cache=False)
408 users_group = cls.get(users_group_id, cache=False)
409
409
410 for k, v in form_data.items():
410 for k, v in form_data.items():
411 if k == 'users_group_members':
411 if k == 'users_group_members':
412 users_group.members = []
412 users_group.members = []
413 Session.flush()
413 Session.flush()
414 members_list = []
414 members_list = []
415 if v:
415 if v:
416 v = [v] if isinstance(v, basestring) else v
416 v = [v] if isinstance(v, basestring) else v
417 for u_id in set(v):
417 for u_id in set(v):
418 member = UserGroupMember(users_group_id, u_id)
418 member = UserGroupMember(users_group_id, u_id)
419 members_list.append(member)
419 members_list.append(member)
420 setattr(users_group, 'members', members_list)
420 setattr(users_group, 'members', members_list)
421 setattr(users_group, k, v)
421 setattr(users_group, k, v)
422
422
423 Session.add(users_group)
423 Session.add(users_group)
424 Session.commit()
424 Session.commit()
425 except:
425 except:
426 log.error(traceback.format_exc())
426 log.error(traceback.format_exc())
427 Session.rollback()
427 Session.rollback()
428 raise
428 raise
429
429
430 @classmethod
430 @classmethod
431 def delete(cls, user_group_id):
431 def delete(cls, user_group_id):
432 try:
432 try:
433
433
434 # check if this group is not assigned to repo
434 # check if this group is not assigned to repo
435 assigned_groups = UserGroupRepoToPerm.query()\
435 assigned_groups = UserGroupRepoToPerm.query()\
436 .filter(UserGroupRepoToPerm.users_group_id ==
436 .filter(UserGroupRepoToPerm.users_group_id ==
437 user_group_id).all()
437 user_group_id).all()
438
438
439 if assigned_groups:
439 if assigned_groups:
440 raise UserGroupAssignedException(
440 raise UserGroupAssignedException(
441 'UserGroup assigned to %s' % assigned_groups)
441 'UserGroup assigned to %s' % assigned_groups)
442
442
443 users_group = cls.get(user_group_id, cache=False)
443 users_group = cls.get(user_group_id, cache=False)
444 Session.delete(users_group)
444 Session.delete(users_group)
445 Session.commit()
445 Session.commit()
446 except:
446 except:
447 log.error(traceback.format_exc())
447 log.error(traceback.format_exc())
448 Session.rollback()
448 Session.rollback()
449 raise
449 raise
450
450
451 class UserGroupMember(Base, BaseModel):
451 class UserGroupMember(Base, BaseModel):
452 __tablename__ = 'users_groups_members'
452 __tablename__ = 'users_groups_members'
453 __table_args__ = {'extend_existing':True}
453 __table_args__ = {'extend_existing':True}
454
454
455 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
455 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
456 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
456 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
458
458
459 user = relationship('User', lazy='joined')
459 user = relationship('User', lazy='joined')
460 users_group = relationship('UserGroup')
460 users_group = relationship('UserGroup')
461
461
462 def __init__(self, gr_id='', u_id=''):
462 def __init__(self, gr_id='', u_id=''):
463 self.users_group_id = gr_id
463 self.users_group_id = gr_id
464 self.user_id = u_id
464 self.user_id = u_id
465
465
466 @staticmethod
466 @staticmethod
467 def add_user_to_group(group, user):
467 def add_user_to_group(group, user):
468 ugm = UserGroupMember()
468 ugm = UserGroupMember()
469 ugm.users_group = group
469 ugm.users_group = group
470 ugm.user = user
470 ugm.user = user
471 Session.add(ugm)
471 Session.add(ugm)
472 Session.commit()
472 Session.commit()
473 return ugm
473 return ugm
474
474
475 class Repository(Base, BaseModel):
475 class Repository(Base, BaseModel):
476 __tablename__ = 'repositories'
476 __tablename__ = 'repositories'
477 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
477 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
478
478
479 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
479 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
480 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
480 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
481 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
481 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
482 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
482 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
483 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
483 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
484 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
484 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
485 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
485 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
486 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
486 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
487 description = Column("description", String(10000), nullable=True, unique=None, default=None)
487 description = Column("description", String(10000), nullable=True, unique=None, default=None)
488 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
488 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
489
489
490 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
490 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
491 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
491 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
492
492
493
493
494 user = relationship('User')
494 user = relationship('User')
495 fork = relationship('Repository', remote_side=repo_id)
495 fork = relationship('Repository', remote_side=repo_id)
496 group = relationship('RepoGroup')
496 group = relationship('RepoGroup')
497 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
497 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
498 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
498 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
499 stats = relationship('Statistics', cascade='all', uselist=False)
499 stats = relationship('Statistics', cascade='all', uselist=False)
500
500
501 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
501 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
502
502
503 logs = relationship('UserLog', cascade='all')
503 logs = relationship('UserLog', cascade='all')
504
504
505 def __repr__(self):
505 def __repr__(self):
506 return "<%s('%s:%s')>" % (self.__class__.__name__,
506 return "<%s('%s:%s')>" % (self.__class__.__name__,
507 self.repo_id, self.repo_name)
507 self.repo_id, self.repo_name)
508
508
509 @classmethod
509 @classmethod
510 def url_sep(cls):
510 def url_sep(cls):
511 return '/'
511 return '/'
512
512
513 @classmethod
513 @classmethod
514 def get_by_repo_name(cls, repo_name):
514 def get_by_repo_name(cls, repo_name):
515 q = Session.query(cls).filter(cls.repo_name == repo_name)
515 q = Session.query(cls).filter(cls.repo_name == repo_name)
516 q = q.options(joinedload(Repository.fork))\
516 q = q.options(joinedload(Repository.fork))\
517 .options(joinedload(Repository.user))\
517 .options(joinedload(Repository.user))\
518 .options(joinedload(Repository.group))
518 .options(joinedload(Repository.group))
519 return q.one()
519 return q.one()
520
520
521 @classmethod
521 @classmethod
522 def get_repo_forks(cls, repo_id):
522 def get_repo_forks(cls, repo_id):
523 return cls.query().filter(Repository.fork_id == repo_id)
523 return cls.query().filter(Repository.fork_id == repo_id)
524
524
525 @classmethod
525 @classmethod
526 def base_path(cls):
526 def base_path(cls):
527 """
527 """
528 Returns base path when all repos are stored
528 Returns base path when all repos are stored
529
529
530 :param cls:
530 :param cls:
531 """
531 """
532 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
532 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
533 cls.url_sep())
533 cls.url_sep())
534 q.options(FromCache("sql_cache_short", "repository_repo_path"))
534 q.options(FromCache("sql_cache_short", "repository_repo_path"))
535 return q.one().ui_value
535 return q.one().ui_value
536
536
537 @property
537 @property
538 def just_name(self):
538 def just_name(self):
539 return self.repo_name.split(Repository.url_sep())[-1]
539 return self.repo_name.split(Repository.url_sep())[-1]
540
540
541 @property
541 @property
542 def groups_with_parents(self):
542 def groups_with_parents(self):
543 groups = []
543 groups = []
544 if self.group is None:
544 if self.group is None:
545 return groups
545 return groups
546
546
547 cur_gr = self.group
547 cur_gr = self.group
548 groups.insert(0, cur_gr)
548 groups.insert(0, cur_gr)
549 while 1:
549 while 1:
550 gr = getattr(cur_gr, 'parent_group', None)
550 gr = getattr(cur_gr, 'parent_group', None)
551 cur_gr = cur_gr.parent_group
551 cur_gr = cur_gr.parent_group
552 if gr is None:
552 if gr is None:
553 break
553 break
554 groups.insert(0, gr)
554 groups.insert(0, gr)
555
555
556 return groups
556 return groups
557
557
558 @property
558 @property
559 def groups_and_repo(self):
559 def groups_and_repo(self):
560 return self.groups_with_parents, self.just_name
560 return self.groups_with_parents, self.just_name
561
561
562 @LazyProperty
562 @LazyProperty
563 def repo_path(self):
563 def repo_path(self):
564 """
564 """
565 Returns base full path for that repository means where it actually
565 Returns base full path for that repository means where it actually
566 exists on a filesystem
566 exists on a filesystem
567 """
567 """
568 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
568 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
569 Repository.url_sep())
569 Repository.url_sep())
570 q.options(FromCache("sql_cache_short", "repository_repo_path"))
570 q.options(FromCache("sql_cache_short", "repository_repo_path"))
571 return q.one().ui_value
571 return q.one().ui_value
572
572
573 @property
573 @property
574 def repo_full_path(self):
574 def repo_full_path(self):
575 p = [self.repo_path]
575 p = [self.repo_path]
576 # we need to split the name by / since this is how we store the
576 # we need to split the name by / since this is how we store the
577 # names in the database, but that eventually needs to be converted
577 # names in the database, but that eventually needs to be converted
578 # into a valid system path
578 # into a valid system path
579 p += self.repo_name.split(Repository.url_sep())
579 p += self.repo_name.split(Repository.url_sep())
580 return os.path.join(*p)
580 return os.path.join(*p)
581
581
582 def get_new_name(self, repo_name):
582 def get_new_name(self, repo_name):
583 """
583 """
584 returns new full repository name based on assigned group and new new
584 returns new full repository name based on assigned group and new new
585
585
586 :param group_name:
586 :param group_name:
587 """
587 """
588 path_prefix = self.group.full_path_splitted if self.group else []
588 path_prefix = self.group.full_path_splitted if self.group else []
589 return Repository.url_sep().join(path_prefix + [repo_name])
589 return Repository.url_sep().join(path_prefix + [repo_name])
590
590
591 @property
591 @property
592 def _config(self):
592 def _config(self):
593 """
593 """
594 Returns db based config object.
594 Returns db based config object.
595 """
595 """
596 from rhodecode.lib.utils import make_db_config
596 from rhodecode.lib.utils import make_db_config
597 return make_db_config(clear_session=False)
597 return make_db_config(clear_session=False)
598
598
599 @classmethod
599 @classmethod
600 def is_valid(cls, repo_name):
600 def is_valid(cls, repo_name):
601 """
601 """
602 returns True if given repo name is a valid filesystem repository
602 returns True if given repo name is a valid filesystem repository
603
603
604 :param cls:
604 :param cls:
605 :param repo_name:
605 :param repo_name:
606 """
606 """
607 from rhodecode.lib.utils import is_valid_repo
607 from rhodecode.lib.utils import is_valid_repo
608
608
609 return is_valid_repo(repo_name, cls.base_path())
609 return is_valid_repo(repo_name, cls.base_path())
610
610
611
611
612 #==========================================================================
612 #==========================================================================
613 # SCM PROPERTIES
613 # SCM PROPERTIES
614 #==========================================================================
614 #==========================================================================
615
615
616 def get_commit(self, rev):
616 def get_commit(self, rev):
617 return get_commit_safe(self.scm_instance, rev)
617 return get_commit_safe(self.scm_instance, rev)
618
618
619 @property
619 @property
620 def tip(self):
620 def tip(self):
621 return self.get_commit('tip')
621 return self.get_commit('tip')
622
622
623 @property
623 @property
624 def author(self):
624 def author(self):
625 return self.tip.author
625 return self.tip.author
626
626
627 @property
627 @property
628 def last_change(self):
628 def last_change(self):
629 return self.scm_instance.last_change
629 return self.scm_instance.last_change
630
630
631 #==========================================================================
631 #==========================================================================
632 # SCM CACHE INSTANCE
632 # SCM CACHE INSTANCE
633 #==========================================================================
633 #==========================================================================
634
634
635 @property
635 @property
636 def invalidate(self):
636 def invalidate(self):
637 return CacheInvalidation.invalidate(self.repo_name)
637 return CacheInvalidation.invalidate(self.repo_name)
638
638
639 def set_invalidate(self):
639 def set_invalidate(self):
640 """
640 """
641 set a cache for invalidation for this instance
641 set a cache for invalidation for this instance
642 """
642 """
643 CacheInvalidation.set_invalidate(self.repo_name)
643 CacheInvalidation.set_invalidate(self.repo_name)
644
644
645 @LazyProperty
645 @LazyProperty
646 def scm_instance(self):
646 def scm_instance(self):
647 return self.__get_instance()
647 return self.__get_instance()
648
648
649 @property
649 @property
650 def scm_instance_cached(self):
650 def scm_instance_cached(self):
651 return self.__get_instance()
651 return self.__get_instance()
652
652
653 def __get_instance(self):
653 def __get_instance(self):
654
654
655 repo_full_path = self.repo_full_path
655 repo_full_path = self.repo_full_path
656
656
657 try:
657 try:
658 alias = get_scm(repo_full_path)[0]
658 alias = get_scm(repo_full_path)[0]
659 log.debug('Creating instance of %s repository' % alias)
659 log.debug('Creating instance of %s repository', alias)
660 backend = get_backend(alias)
660 backend = get_backend(alias)
661 except VCSError:
661 except VCSError:
662 log.error(traceback.format_exc())
662 log.error(traceback.format_exc())
663 log.error('Perhaps this repository is in db and not in '
663 log.error('Perhaps this repository is in db and not in '
664 'filesystem run rescan repositories with '
664 'filesystem run rescan repositories with '
665 '"destroy old data " option from admin panel')
665 '"destroy old data " option from admin panel')
666 return
666 return
667
667
668 if alias == 'hg':
668 if alias == 'hg':
669
669
670 repo = backend(safe_str(repo_full_path), create=False,
670 repo = backend(safe_str(repo_full_path), create=False,
671 config=self._config)
671 config=self._config)
672
672
673 else:
673 else:
674 repo = backend(repo_full_path, create=False)
674 repo = backend(repo_full_path, create=False)
675
675
676 return repo
676 return repo
677
677
678
678
679 class Group(Base, BaseModel):
679 class Group(Base, BaseModel):
680 __tablename__ = 'groups'
680 __tablename__ = 'groups'
681 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
681 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
682 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
682 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
683 __mapper_args__ = {'order_by':'group_name'}
683 __mapper_args__ = {'order_by':'group_name'}
684
684
685 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
685 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
686 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
686 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
687 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
687 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
688 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
688 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
689
689
690 parent_group = relationship('Group', remote_side=group_id)
690 parent_group = relationship('Group', remote_side=group_id)
691
691
692 def __init__(self, group_name='', parent_group=None):
692 def __init__(self, group_name='', parent_group=None):
693 self.group_name = group_name
693 self.group_name = group_name
694 self.parent_group = parent_group
694 self.parent_group = parent_group
695
695
696 def __repr__(self):
696 def __repr__(self):
697 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
697 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
698 self.group_name)
698 self.group_name)
699
699
700 @classmethod
700 @classmethod
701 def url_sep(cls):
701 def url_sep(cls):
702 return '/'
702 return '/'
703
703
704 @classmethod
704 @classmethod
705 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
705 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
706 if case_insensitive:
706 if case_insensitive:
707 gr = cls.query()\
707 gr = cls.query()\
708 .filter(cls.group_name.ilike(group_name))
708 .filter(cls.group_name.ilike(group_name))
709 else:
709 else:
710 gr = cls.query()\
710 gr = cls.query()\
711 .filter(cls.group_name == group_name)
711 .filter(cls.group_name == group_name)
712 if cache:
712 if cache:
713 gr = gr.options(FromCache("sql_cache_short",
713 gr = gr.options(FromCache("sql_cache_short",
714 "get_group_%s" % group_name))
714 "get_group_%s" % group_name))
715 return gr.scalar()
715 return gr.scalar()
716
716
717 @property
717 @property
718 def parents(self):
718 def parents(self):
719 parents_recursion_limit = 5
719 parents_recursion_limit = 5
720 groups = []
720 groups = []
721 if self.parent_group is None:
721 if self.parent_group is None:
722 return groups
722 return groups
723 cur_gr = self.parent_group
723 cur_gr = self.parent_group
724 groups.insert(0, cur_gr)
724 groups.insert(0, cur_gr)
725 cnt = 0
725 cnt = 0
726 while 1:
726 while 1:
727 cnt += 1
727 cnt += 1
728 gr = getattr(cur_gr, 'parent_group', None)
728 gr = getattr(cur_gr, 'parent_group', None)
729 cur_gr = cur_gr.parent_group
729 cur_gr = cur_gr.parent_group
730 if gr is None:
730 if gr is None:
731 break
731 break
732 if cnt == parents_recursion_limit:
732 if cnt == parents_recursion_limit:
733 # this will prevent accidental infinit loops
733 # this will prevent accidental infinit loops
734 log.error('group nested more than %s' %
734 log.error('group nested more than %s',
735 parents_recursion_limit)
735 parents_recursion_limit)
736 break
736 break
737
737
738 groups.insert(0, gr)
738 groups.insert(0, gr)
739 return groups
739 return groups
740
740
741 @property
741 @property
742 def children(self):
742 def children(self):
743 return Group.query().filter(Group.parent_group == self)
743 return Group.query().filter(Group.parent_group == self)
744
744
745 @property
745 @property
746 def name(self):
746 def name(self):
747 return self.group_name.split(Group.url_sep())[-1]
747 return self.group_name.split(Group.url_sep())[-1]
748
748
749 @property
749 @property
750 def full_path(self):
750 def full_path(self):
751 return self.group_name
751 return self.group_name
752
752
753 @property
753 @property
754 def full_path_splitted(self):
754 def full_path_splitted(self):
755 return self.group_name.split(Group.url_sep())
755 return self.group_name.split(Group.url_sep())
756
756
757 @property
757 @property
758 def repositories(self):
758 def repositories(self):
759 return Repository.query().filter(Repository.group == self)
759 return Repository.query().filter(Repository.group == self)
760
760
761 @property
761 @property
762 def repositories_recursive_count(self):
762 def repositories_recursive_count(self):
763 cnt = self.repositories.count()
763 cnt = self.repositories.count()
764
764
765 def children_count(group):
765 def children_count(group):
766 cnt = 0
766 cnt = 0
767 for child in group.children:
767 for child in group.children:
768 cnt += child.repositories.count()
768 cnt += child.repositories.count()
769 cnt += children_count(child)
769 cnt += children_count(child)
770 return cnt
770 return cnt
771
771
772 return cnt + children_count(self)
772 return cnt + children_count(self)
773
773
774
774
775 def get_new_name(self, group_name):
775 def get_new_name(self, group_name):
776 """
776 """
777 returns new full group name based on parent and new name
777 returns new full group name based on parent and new name
778
778
779 :param group_name:
779 :param group_name:
780 """
780 """
781 path_prefix = (self.parent_group.full_path_splitted if
781 path_prefix = (self.parent_group.full_path_splitted if
782 self.parent_group else [])
782 self.parent_group else [])
783 return Group.url_sep().join(path_prefix + [group_name])
783 return Group.url_sep().join(path_prefix + [group_name])
784
784
785
785
786 class Permission(Base, BaseModel):
786 class Permission(Base, BaseModel):
787 __tablename__ = 'permissions'
787 __tablename__ = 'permissions'
788 __table_args__ = {'extend_existing':True}
788 __table_args__ = {'extend_existing':True}
789 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
789 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
790 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
790 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
791 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
791 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
792
792
793 def __repr__(self):
793 def __repr__(self):
794 return "<%s('%s:%s')>" % (self.__class__.__name__,
794 return "<%s('%s:%s')>" % (self.__class__.__name__,
795 self.permission_id, self.permission_name)
795 self.permission_id, self.permission_name)
796
796
797 @classmethod
797 @classmethod
798 def get_by_key(cls, key):
798 def get_by_key(cls, key):
799 return cls.query().filter(cls.permission_name == key).scalar()
799 return cls.query().filter(cls.permission_name == key).scalar()
800
800
801 class UserRepoToPerm(Base, BaseModel):
801 class UserRepoToPerm(Base, BaseModel):
802 __tablename__ = 'repo_to_perm'
802 __tablename__ = 'repo_to_perm'
803 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
803 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
804 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
804 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
805 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
805 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
806 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
806 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
807 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
807 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
808
808
809 user = relationship('User')
809 user = relationship('User')
810 permission = relationship('Permission')
810 permission = relationship('Permission')
811 repository = relationship('Repository')
811 repository = relationship('Repository')
812
812
813 class UserToPerm(Base, BaseModel):
813 class UserToPerm(Base, BaseModel):
814 __tablename__ = 'user_to_perm'
814 __tablename__ = 'user_to_perm'
815 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
815 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
816 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
816 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
817 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
817 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
819
819
820 user = relationship('User')
820 user = relationship('User')
821 permission = relationship('Permission')
821 permission = relationship('Permission')
822
822
823 @classmethod
823 @classmethod
824 def has_perm(cls, user_id, perm):
824 def has_perm(cls, user_id, perm):
825 if not isinstance(perm, Permission):
825 if not isinstance(perm, Permission):
826 raise Exception('perm needs to be an instance of Permission class')
826 raise Exception('perm needs to be an instance of Permission class')
827
827
828 return cls.query().filter(cls.user_id == user_id)\
828 return cls.query().filter(cls.user_id == user_id)\
829 .filter(cls.permission == perm).scalar() is not None
829 .filter(cls.permission == perm).scalar() is not None
830
830
831 @classmethod
831 @classmethod
832 def grant_perm(cls, user_id, perm):
832 def grant_perm(cls, user_id, perm):
833 if not isinstance(perm, Permission):
833 if not isinstance(perm, Permission):
834 raise Exception('perm needs to be an instance of Permission class')
834 raise Exception('perm needs to be an instance of Permission class')
835
835
836 new = cls()
836 new = cls()
837 new.user_id = user_id
837 new.user_id = user_id
838 new.permission = perm
838 new.permission = perm
839 try:
839 try:
840 Session.add(new)
840 Session.add(new)
841 Session.commit()
841 Session.commit()
842 except:
842 except:
843 Session.rollback()
843 Session.rollback()
844
844
845
845
846 @classmethod
846 @classmethod
847 def revoke_perm(cls, user_id, perm):
847 def revoke_perm(cls, user_id, perm):
848 if not isinstance(perm, Permission):
848 if not isinstance(perm, Permission):
849 raise Exception('perm needs to be an instance of Permission class')
849 raise Exception('perm needs to be an instance of Permission class')
850
850
851 try:
851 try:
852 cls.query().filter(cls.user_id == user_id) \
852 cls.query().filter(cls.user_id == user_id) \
853 .filter(cls.permission == perm).delete()
853 .filter(cls.permission == perm).delete()
854 Session.commit()
854 Session.commit()
855 except:
855 except:
856 Session.rollback()
856 Session.rollback()
857
857
858 class UserGroupRepoToPerm(Base, BaseModel):
858 class UserGroupRepoToPerm(Base, BaseModel):
859 __tablename__ = 'users_group_repo_to_perm'
859 __tablename__ = 'users_group_repo_to_perm'
860 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
860 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
861 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
861 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
862 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
862 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
863 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
863 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
864 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
864 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
865
865
866 users_group = relationship('UserGroup')
866 users_group = relationship('UserGroup')
867 permission = relationship('Permission')
867 permission = relationship('Permission')
868 repository = relationship('Repository')
868 repository = relationship('Repository')
869
869
870 def __repr__(self):
870 def __repr__(self):
871 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
871 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
872
872
873 class UserGroupToPerm(Base, BaseModel):
873 class UserGroupToPerm(Base, BaseModel):
874 __tablename__ = 'users_group_to_perm'
874 __tablename__ = 'users_group_to_perm'
875 __table_args__ = {'extend_existing':True}
875 __table_args__ = {'extend_existing':True}
876 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
876 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
877 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
877 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
878 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
878 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
879
879
880 users_group = relationship('UserGroup')
880 users_group = relationship('UserGroup')
881 permission = relationship('Permission')
881 permission = relationship('Permission')
882
882
883
883
884 @classmethod
884 @classmethod
885 def has_perm(cls, users_group_id, perm):
885 def has_perm(cls, users_group_id, perm):
886 if not isinstance(perm, Permission):
886 if not isinstance(perm, Permission):
887 raise Exception('perm needs to be an instance of Permission class')
887 raise Exception('perm needs to be an instance of Permission class')
888
888
889 return cls.query().filter(cls.users_group_id ==
889 return cls.query().filter(cls.users_group_id ==
890 users_group_id)\
890 users_group_id)\
891 .filter(cls.permission == perm)\
891 .filter(cls.permission == perm)\
892 .scalar() is not None
892 .scalar() is not None
893
893
894 @classmethod
894 @classmethod
895 def grant_perm(cls, users_group_id, perm):
895 def grant_perm(cls, users_group_id, perm):
896 if not isinstance(perm, Permission):
896 if not isinstance(perm, Permission):
897 raise Exception('perm needs to be an instance of Permission class')
897 raise Exception('perm needs to be an instance of Permission class')
898
898
899 new = cls()
899 new = cls()
900 new.users_group_id = users_group_id
900 new.users_group_id = users_group_id
901 new.permission = perm
901 new.permission = perm
902 try:
902 try:
903 Session.add(new)
903 Session.add(new)
904 Session.commit()
904 Session.commit()
905 except:
905 except:
906 Session.rollback()
906 Session.rollback()
907
907
908
908
909 @classmethod
909 @classmethod
910 def revoke_perm(cls, users_group_id, perm):
910 def revoke_perm(cls, users_group_id, perm):
911 if not isinstance(perm, Permission):
911 if not isinstance(perm, Permission):
912 raise Exception('perm needs to be an instance of Permission class')
912 raise Exception('perm needs to be an instance of Permission class')
913
913
914 try:
914 try:
915 cls.query().filter(cls.users_group_id == users_group_id) \
915 cls.query().filter(cls.users_group_id == users_group_id) \
916 .filter(cls.permission == perm).delete()
916 .filter(cls.permission == perm).delete()
917 Session.commit()
917 Session.commit()
918 except:
918 except:
919 Session.rollback()
919 Session.rollback()
920
920
921
921
922 class UserRepoGroupToPerm(Base, BaseModel):
922 class UserRepoGroupToPerm(Base, BaseModel):
923 __tablename__ = 'group_to_perm'
923 __tablename__ = 'group_to_perm'
924 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
924 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
925
925
926 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
926 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
927 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
927 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
928 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
928 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
929 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
929 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
930
930
931 user = relationship('User')
931 user = relationship('User')
932 permission = relationship('Permission')
932 permission = relationship('Permission')
933 group = relationship('RepoGroup')
933 group = relationship('RepoGroup')
934
934
935 class Statistics(Base, BaseModel):
935 class Statistics(Base, BaseModel):
936 __tablename__ = 'statistics'
936 __tablename__ = 'statistics'
937 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
937 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
938 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
938 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
939 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
939 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
940 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
940 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
941 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
941 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
942 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
942 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
943 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
943 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
944
944
945 repository = relationship('Repository', single_parent=True)
945 repository = relationship('Repository', single_parent=True)
946
946
947 class UserFollowing(Base, BaseModel):
947 class UserFollowing(Base, BaseModel):
948 __tablename__ = 'user_followings'
948 __tablename__ = 'user_followings'
949 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
949 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
950 UniqueConstraint('user_id', 'follows_user_id')
950 UniqueConstraint('user_id', 'follows_user_id')
951 , {'extend_existing':True})
951 , {'extend_existing':True})
952
952
953 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
953 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
954 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
954 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
955 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
955 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
956 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
956 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
957 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
957 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
958
958
959 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
959 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
960
960
961 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
961 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
962 follows_repository = relationship('Repository', order_by='Repository.repo_name')
962 follows_repository = relationship('Repository', order_by='Repository.repo_name')
963
963
964
964
965 @classmethod
965 @classmethod
966 def get_repo_followers(cls, repo_id):
966 def get_repo_followers(cls, repo_id):
967 return cls.query().filter(cls.follows_repo_id == repo_id)
967 return cls.query().filter(cls.follows_repo_id == repo_id)
968
968
969 class CacheInvalidation(Base, BaseModel):
969 class CacheInvalidation(Base, BaseModel):
970 __tablename__ = 'cache_invalidation'
970 __tablename__ = 'cache_invalidation'
971 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
971 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
972 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
973 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
974 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
974 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
975 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
975 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
976
976
977
977
978 def __init__(self, cache_key, cache_args=''):
978 def __init__(self, cache_key, cache_args=''):
979 self.cache_key = cache_key
979 self.cache_key = cache_key
980 self.cache_args = cache_args
980 self.cache_args = cache_args
981 self.cache_active = False
981 self.cache_active = False
982
982
983 def __repr__(self):
983 def __repr__(self):
984 return "<%s('%s:%s')>" % (self.__class__.__name__,
984 return "<%s('%s:%s')>" % (self.__class__.__name__,
985 self.cache_id, self.cache_key)
985 self.cache_id, self.cache_key)
986
986
987 @classmethod
987 @classmethod
988 def invalidate(cls, key):
988 def invalidate(cls, key):
989 """
989 """
990 Returns Invalidation object if this given key should be invalidated
990 Returns Invalidation object if this given key should be invalidated
991 None otherwise. `cache_active = False` means that this cache
991 None otherwise. `cache_active = False` means that this cache
992 state is not valid and needs to be invalidated
992 state is not valid and needs to be invalidated
993
993
994 :param key:
994 :param key:
995 """
995 """
996 return cls.query()\
996 return cls.query()\
997 .filter(CacheInvalidation.cache_key == key)\
997 .filter(CacheInvalidation.cache_key == key)\
998 .filter(CacheInvalidation.cache_active == False)\
998 .filter(CacheInvalidation.cache_active == False)\
999 .scalar()
999 .scalar()
1000
1000
1001 @classmethod
1001 @classmethod
1002 def set_invalidate(cls, key):
1002 def set_invalidate(cls, key):
1003 """
1003 """
1004 Mark this Cache key for invalidation
1004 Mark this Cache key for invalidation
1005
1005
1006 :param key:
1006 :param key:
1007 """
1007 """
1008
1008
1009 log.debug('marking %s for invalidation' % key)
1009 log.debug('marking %s for invalidation', key)
1010 inv_obj = Session.query(cls)\
1010 inv_obj = Session.query(cls)\
1011 .filter(cls.cache_key == key).scalar()
1011 .filter(cls.cache_key == key).scalar()
1012 if inv_obj:
1012 if inv_obj:
1013 inv_obj.cache_active = False
1013 inv_obj.cache_active = False
1014 else:
1014 else:
1015 log.debug('cache key not found in invalidation db -> creating one')
1015 log.debug('cache key not found in invalidation db -> creating one')
1016 inv_obj = CacheInvalidation(key)
1016 inv_obj = CacheInvalidation(key)
1017
1017
1018 try:
1018 try:
1019 Session.add(inv_obj)
1019 Session.add(inv_obj)
1020 Session.commit()
1020 Session.commit()
1021 except Exception:
1021 except Exception:
1022 log.error(traceback.format_exc())
1022 log.error(traceback.format_exc())
1023 Session.rollback()
1023 Session.rollback()
1024
1024
1025 @classmethod
1025 @classmethod
1026 def set_valid(cls, key):
1026 def set_valid(cls, key):
1027 """
1027 """
1028 Mark this cache key as active and currently cached
1028 Mark this cache key as active and currently cached
1029
1029
1030 :param key:
1030 :param key:
1031 """
1031 """
1032 inv_obj = Session.query(CacheInvalidation)\
1032 inv_obj = Session.query(CacheInvalidation)\
1033 .filter(CacheInvalidation.cache_key == key).scalar()
1033 .filter(CacheInvalidation.cache_key == key).scalar()
1034 inv_obj.cache_active = True
1034 inv_obj.cache_active = True
1035 Session.add(inv_obj)
1035 Session.add(inv_obj)
1036 Session.commit()
1036 Session.commit()
1037
1037
1038 class DbMigrateVersion(Base, BaseModel):
1038 class DbMigrateVersion(Base, BaseModel):
1039 __tablename__ = 'db_migrate_version'
1039 __tablename__ = 'db_migrate_version'
1040 __table_args__ = {'extend_existing':True}
1040 __table_args__ = {'extend_existing':True}
1041 repository_id = Column('repository_id', String(250), primary_key=True)
1041 repository_id = Column('repository_id', String(250), primary_key=True)
1042 repository_path = Column('repository_path', Text)
1042 repository_path = Column('repository_path', Text)
1043 version = Column('version', Integer)
1043 version = Column('version', Integer)
@@ -1,1266 +1,1264 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 logging
22 import logging
23 import datetime
23 import datetime
24 import traceback
24 import traceback
25 from collections import defaultdict
25 from collections import defaultdict
26
26
27 from sqlalchemy import *
27 from sqlalchemy import *
28 from sqlalchemy.ext.hybrid import hybrid_property
28 from sqlalchemy.ext.hybrid import hybrid_property
29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
30 from beaker.cache import cache_region, region_invalidate
30 from beaker.cache import cache_region, region_invalidate
31
31
32 from rhodecode.lib.vcs import get_backend
32 from rhodecode.lib.vcs import get_backend
33 from rhodecode.lib.vcs.utils.helpers import get_scm
33 from rhodecode.lib.vcs.utils.helpers import get_scm
34 from rhodecode.lib.vcs.exceptions import VCSError
34 from rhodecode.lib.vcs.exceptions import VCSError
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36
36
37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, \
37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, \
38 safe_unicode
38 safe_unicode
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.caching_query import FromCache
40 from rhodecode.lib.caching_query import FromCache
41
41
42 from rhodecode.model.meta import Base, Session
42 from rhodecode.model.meta import Base, Session
43 import hashlib
43 import hashlib
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48 #==============================================================================
48 #==============================================================================
49 # BASE CLASSES
49 # BASE CLASSES
50 #==============================================================================
50 #==============================================================================
51
51
52 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
52 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
53
53
54
54
55 class ModelSerializer(json.JSONEncoder):
55 class ModelSerializer(json.JSONEncoder):
56 """
56 """
57 Simple Serializer for JSON,
57 Simple Serializer for JSON,
58
58
59 usage::
59 usage::
60
60
61 to make object customized for serialization implement a __json__
61 to make object customized for serialization implement a __json__
62 method that will return a dict for serialization into json
62 method that will return a dict for serialization into json
63
63
64 example::
64 example::
65
65
66 class Task(object):
66 class Task(object):
67
67
68 def __init__(self, name, value):
68 def __init__(self, name, value):
69 self.name = name
69 self.name = name
70 self.value = value
70 self.value = value
71
71
72 def __json__(self):
72 def __json__(self):
73 return dict(name=self.name,
73 return dict(name=self.name,
74 value=self.value)
74 value=self.value)
75
75
76 """
76 """
77
77
78 def default(self, obj):
78 def default(self, obj):
79
79
80 if hasattr(obj, '__json__'):
80 if hasattr(obj, '__json__'):
81 return obj.__json__()
81 return obj.__json__()
82 else:
82 else:
83 return json.JSONEncoder.default(self, obj)
83 return json.JSONEncoder.default(self, obj)
84
84
85
85
86 class BaseModel(object):
86 class BaseModel(object):
87 """
87 """
88 Base Model for all classess
88 Base Model for all classess
89 """
89 """
90
90
91 @classmethod
91 @classmethod
92 def _get_keys(cls):
92 def _get_keys(cls):
93 """return column names for this model """
93 """return column names for this model """
94 return class_mapper(cls).c.keys()
94 return class_mapper(cls).c.keys()
95
95
96 def get_dict(self):
96 def get_dict(self):
97 """
97 """
98 return dict with keys and values corresponding
98 return dict with keys and values corresponding
99 to this model data """
99 to this model data """
100
100
101 d = {}
101 d = {}
102 for k in self._get_keys():
102 for k in self._get_keys():
103 d[k] = getattr(self, k)
103 d[k] = getattr(self, k)
104
104
105 # also use __json__() if present to get additional fields
105 # also use __json__() if present to get additional fields
106 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
106 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
107 d[k] = val
107 d[k] = val
108 return d
108 return d
109
109
110 def get_appstruct(self):
110 def get_appstruct(self):
111 """return list with keys and values tupples corresponding
111 """return list with keys and values tupples corresponding
112 to this model data """
112 to this model data """
113
113
114 l = []
114 l = []
115 for k in self._get_keys():
115 for k in self._get_keys():
116 l.append((k, getattr(self, k),))
116 l.append((k, getattr(self, k),))
117 return l
117 return l
118
118
119 def populate_obj(self, populate_dict):
119 def populate_obj(self, populate_dict):
120 """populate model with data from given populate_dict"""
120 """populate model with data from given populate_dict"""
121
121
122 for k in self._get_keys():
122 for k in self._get_keys():
123 if k in populate_dict:
123 if k in populate_dict:
124 setattr(self, k, populate_dict[k])
124 setattr(self, k, populate_dict[k])
125
125
126 @classmethod
126 @classmethod
127 def query(cls):
127 def query(cls):
128 return Session.query(cls)
128 return Session.query(cls)
129
129
130 @classmethod
130 @classmethod
131 def get(cls, id_):
131 def get(cls, id_):
132 if id_:
132 if id_:
133 return cls.query().get(id_)
133 return cls.query().get(id_)
134
134
135 @classmethod
135 @classmethod
136 def getAll(cls):
136 def getAll(cls):
137 return cls.query().all()
137 return cls.query().all()
138
138
139 @classmethod
139 @classmethod
140 def delete(cls, id_):
140 def delete(cls, id_):
141 obj = cls.query().get(id_)
141 obj = cls.query().get(id_)
142 Session.delete(obj)
142 Session.delete(obj)
143
143
144 def __repr__(self):
144 def __repr__(self):
145 if hasattr(self, '__unicode__'):
145 if hasattr(self, '__unicode__'):
146 # python repr needs to return str
146 # python repr needs to return str
147 return safe_str(self.__unicode__())
147 return safe_str(self.__unicode__())
148 return '<DB:%s>' % (self.__class__.__name__)
148 return '<DB:%s>' % (self.__class__.__name__)
149
149
150
150
151 class RhodeCodeSetting(Base, BaseModel):
151 class RhodeCodeSetting(Base, BaseModel):
152 __tablename__ = 'rhodecode_settings'
152 __tablename__ = 'rhodecode_settings'
153 __table_args__ = (
153 __table_args__ = (
154 UniqueConstraint('app_settings_name'),
154 UniqueConstraint('app_settings_name'),
155 {'extend_existing': True, 'mysql_engine':'InnoDB',
155 {'extend_existing': True, 'mysql_engine':'InnoDB',
156 'mysql_charset': 'utf8'}
156 'mysql_charset': 'utf8'}
157 )
157 )
158 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
158 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
159 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
159 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
160 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
160 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
161
161
162 def __init__(self, k='', v=''):
162 def __init__(self, k='', v=''):
163 self.app_settings_name = k
163 self.app_settings_name = k
164 self.app_settings_value = v
164 self.app_settings_value = v
165
165
166 @validates('_app_settings_value')
166 @validates('_app_settings_value')
167 def validate_settings_value(self, key, val):
167 def validate_settings_value(self, key, val):
168 assert type(val) == unicode
168 assert type(val) == unicode
169 return val
169 return val
170
170
171 @hybrid_property
171 @hybrid_property
172 def app_settings_value(self):
172 def app_settings_value(self):
173 v = self._app_settings_value
173 v = self._app_settings_value
174 if self.app_settings_name == 'ldap_active':
174 if self.app_settings_name == 'ldap_active':
175 v = str2bool(v)
175 v = str2bool(v)
176 return v
176 return v
177
177
178 @app_settings_value.setter
178 @app_settings_value.setter
179 def app_settings_value(self, val):
179 def app_settings_value(self, val):
180 """
180 """
181 Setter that will always make sure we use unicode in app_settings_value
181 Setter that will always make sure we use unicode in app_settings_value
182
182
183 :param val:
183 :param val:
184 """
184 """
185 self._app_settings_value = safe_unicode(val)
185 self._app_settings_value = safe_unicode(val)
186
186
187 def __unicode__(self):
187 def __unicode__(self):
188 return u"<%s('%s:%s')>" % (
188 return u"<%s('%s:%s')>" % (
189 self.__class__.__name__,
189 self.__class__.__name__,
190 self.app_settings_name, self.app_settings_value
190 self.app_settings_name, self.app_settings_value
191 )
191 )
192
192
193 @classmethod
193 @classmethod
194 def get_by_name(cls, ldap_key):
194 def get_by_name(cls, ldap_key):
195 return cls.query()\
195 return cls.query()\
196 .filter(cls.app_settings_name == ldap_key).scalar()
196 .filter(cls.app_settings_name == ldap_key).scalar()
197
197
198 @classmethod
198 @classmethod
199 def get_app_settings(cls, cache=False):
199 def get_app_settings(cls, cache=False):
200
200
201 ret = cls.query()
201 ret = cls.query()
202
202
203 if cache:
203 if cache:
204 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
204 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
205
205
206 if not ret:
206 if not ret:
207 raise Exception('Could not get application settings !')
207 raise Exception('Could not get application settings !')
208 settings = {}
208 settings = {}
209 for each in ret:
209 for each in ret:
210 settings['rhodecode_' + each.app_settings_name] = \
210 settings['rhodecode_' + each.app_settings_name] = \
211 each.app_settings_value
211 each.app_settings_value
212
212
213 return settings
213 return settings
214
214
215 @classmethod
215 @classmethod
216 def get_ldap_settings(cls, cache=False):
216 def get_ldap_settings(cls, cache=False):
217 ret = cls.query()\
217 ret = cls.query()\
218 .filter(cls.app_settings_name.startswith('ldap_')).all()
218 .filter(cls.app_settings_name.startswith('ldap_')).all()
219 fd = {}
219 fd = {}
220 for row in ret:
220 for row in ret:
221 fd.update({row.app_settings_name:row.app_settings_value})
221 fd.update({row.app_settings_name:row.app_settings_value})
222
222
223 return fd
223 return fd
224
224
225
225
226 class RhodeCodeUi(Base, BaseModel):
226 class RhodeCodeUi(Base, BaseModel):
227 __tablename__ = 'rhodecode_ui'
227 __tablename__ = 'rhodecode_ui'
228 __table_args__ = (
228 __table_args__ = (
229 UniqueConstraint('ui_key'),
229 UniqueConstraint('ui_key'),
230 {'extend_existing': True, 'mysql_engine':'InnoDB',
230 {'extend_existing': True, 'mysql_engine':'InnoDB',
231 'mysql_charset': 'utf8'}
231 'mysql_charset': 'utf8'}
232 )
232 )
233
233
234 HOOK_REPO_SIZE = 'changegroup.repo_size'
234 HOOK_REPO_SIZE = 'changegroup.repo_size'
235 HOOK_PUSH = 'pretxnchangegroup.push_logger'
235 HOOK_PUSH = 'pretxnchangegroup.push_logger'
236 HOOK_PULL = 'preoutgoing.pull_logger'
236 HOOK_PULL = 'preoutgoing.pull_logger'
237
237
238 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
238 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
239 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
239 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
240 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
240 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
241 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
241 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
242 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
242 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
243
243
244 @classmethod
244 @classmethod
245 def get_by_key(cls, key):
245 def get_by_key(cls, key):
246 return cls.query().filter(cls.ui_key == key)
246 return cls.query().filter(cls.ui_key == key)
247
247
248 @classmethod
248 @classmethod
249 def get_builtin_hooks(cls):
249 def get_builtin_hooks(cls):
250 q = cls.query()
250 q = cls.query()
251 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
251 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
252 cls.HOOK_PUSH, cls.HOOK_PULL]))
252 cls.HOOK_PUSH, cls.HOOK_PULL]))
253 return q.all()
253 return q.all()
254
254
255 @classmethod
255 @classmethod
256 def get_custom_hooks(cls):
256 def get_custom_hooks(cls):
257 q = cls.query()
257 q = cls.query()
258 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
258 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
259 cls.HOOK_PUSH, cls.HOOK_PULL]))
259 cls.HOOK_PUSH, cls.HOOK_PULL]))
260 q = q.filter(cls.ui_section == 'hooks')
260 q = q.filter(cls.ui_section == 'hooks')
261 return q.all()
261 return q.all()
262
262
263 @classmethod
263 @classmethod
264 def create_or_update_hook(cls, key, val):
264 def create_or_update_hook(cls, key, val):
265 new_ui = cls.get_by_key(key).scalar() or cls()
265 new_ui = cls.get_by_key(key).scalar() or cls()
266 new_ui.ui_section = 'hooks'
266 new_ui.ui_section = 'hooks'
267 new_ui.ui_active = True
267 new_ui.ui_active = True
268 new_ui.ui_key = key
268 new_ui.ui_key = key
269 new_ui.ui_value = val
269 new_ui.ui_value = val
270
270
271 Session.add(new_ui)
271 Session.add(new_ui)
272
272
273
273
274 class User(Base, BaseModel):
274 class User(Base, BaseModel):
275 __tablename__ = 'users'
275 __tablename__ = 'users'
276 __table_args__ = (
276 __table_args__ = (
277 UniqueConstraint('username'), UniqueConstraint('email'),
277 UniqueConstraint('username'), UniqueConstraint('email'),
278 {'extend_existing': True, 'mysql_engine':'InnoDB',
278 {'extend_existing': True, 'mysql_engine':'InnoDB',
279 'mysql_charset': 'utf8'}
279 'mysql_charset': 'utf8'}
280 )
280 )
281 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
282 username = Column("username", String(255), nullable=True, unique=None, default=None)
282 username = Column("username", String(255), nullable=True, unique=None, default=None)
283 password = Column("password", String(255), nullable=True, unique=None, default=None)
283 password = Column("password", String(255), nullable=True, unique=None, default=None)
284 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
284 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
285 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
285 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
286 name = Column("name", String(255), nullable=True, unique=None, default=None)
286 name = Column("name", String(255), nullable=True, unique=None, default=None)
287 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
287 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
288 _email = Column("email", String(255), nullable=True, unique=None, default=None)
288 _email = Column("email", String(255), nullable=True, unique=None, default=None)
289 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
289 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
290 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
290 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
291 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
291 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
292
292
293 user_log = relationship('UserLog', cascade='all')
293 user_log = relationship('UserLog', cascade='all')
294 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
294 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
295
295
296 repositories = relationship('Repository')
296 repositories = relationship('Repository')
297 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
297 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
298 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
298 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
299 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
299 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
300
300
301 group_member = relationship('UserGroupMember', cascade='all')
301 group_member = relationship('UserGroupMember', cascade='all')
302
302
303 notifications = relationship('UserNotification', cascade='all')
303 notifications = relationship('UserNotification', cascade='all')
304 # notifications assigned to this user
304 # notifications assigned to this user
305 user_created_notifications = relationship('Notification', cascade='all')
305 user_created_notifications = relationship('Notification', cascade='all')
306 # comments created by this user
306 # comments created by this user
307 user_comments = relationship('ChangesetComment', cascade='all')
307 user_comments = relationship('ChangesetComment', cascade='all')
308
308
309 @hybrid_property
309 @hybrid_property
310 def email(self):
310 def email(self):
311 return self._email
311 return self._email
312
312
313 @email.setter
313 @email.setter
314 def email(self, val):
314 def email(self, val):
315 self._email = val.lower() if val else None
315 self._email = val.lower() if val else None
316
316
317 @property
317 @property
318 def full_name(self):
318 def full_name(self):
319 return '%s %s' % (self.name, self.lastname)
319 return '%s %s' % (self.name, self.lastname)
320
320
321 @property
321 @property
322 def full_name_or_username(self):
322 def full_name_or_username(self):
323 return ('%s %s' % (self.name, self.lastname)
323 return ('%s %s' % (self.name, self.lastname)
324 if (self.name and self.lastname) else self.username)
324 if (self.name and self.lastname) else self.username)
325
325
326 @property
326 @property
327 def full_contact(self):
327 def full_contact(self):
328 return '%s %s <%s>' % (self.name, self.lastname, self.email)
328 return '%s %s <%s>' % (self.name, self.lastname, self.email)
329
329
330 @property
330 @property
331 def short_contact(self):
331 def short_contact(self):
332 return '%s %s' % (self.name, self.lastname)
332 return '%s %s' % (self.name, self.lastname)
333
333
334 @property
334 @property
335 def is_admin(self):
335 def is_admin(self):
336 return self.admin
336 return self.admin
337
337
338 def __unicode__(self):
338 def __unicode__(self):
339 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
339 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
340 self.user_id, self.username)
340 self.user_id, self.username)
341
341
342 @classmethod
342 @classmethod
343 def get_by_username(cls, username, case_insensitive=False, cache=False):
343 def get_by_username(cls, username, case_insensitive=False, cache=False):
344 if case_insensitive:
344 if case_insensitive:
345 q = cls.query().filter(cls.username.ilike(username))
345 q = cls.query().filter(cls.username.ilike(username))
346 else:
346 else:
347 q = cls.query().filter(cls.username == username)
347 q = cls.query().filter(cls.username == username)
348
348
349 if cache:
349 if cache:
350 q = q.options(FromCache(
350 q = q.options(FromCache(
351 "sql_cache_short",
351 "sql_cache_short",
352 "get_user_%s" % _hash_key(username)
352 "get_user_%s" % _hash_key(username)
353 )
353 )
354 )
354 )
355 return q.scalar()
355 return q.scalar()
356
356
357 @classmethod
357 @classmethod
358 def get_by_auth_token(cls, auth_token, cache=False):
358 def get_by_auth_token(cls, auth_token, cache=False):
359 q = cls.query().filter(cls.api_key == auth_token)
359 q = cls.query().filter(cls.api_key == auth_token)
360
360
361 if cache:
361 if cache:
362 q = q.options(FromCache("sql_cache_short",
362 q = q.options(FromCache("sql_cache_short",
363 "get_auth_token_%s" % auth_token))
363 "get_auth_token_%s" % auth_token))
364 return q.scalar()
364 return q.scalar()
365
365
366 @classmethod
366 @classmethod
367 def get_by_email(cls, email, case_insensitive=False, cache=False):
367 def get_by_email(cls, email, case_insensitive=False, cache=False):
368 if case_insensitive:
368 if case_insensitive:
369 q = cls.query().filter(cls.email.ilike(email))
369 q = cls.query().filter(cls.email.ilike(email))
370 else:
370 else:
371 q = cls.query().filter(cls.email == email)
371 q = cls.query().filter(cls.email == email)
372
372
373 if cache:
373 if cache:
374 q = q.options(FromCache("sql_cache_short",
374 q = q.options(FromCache("sql_cache_short",
375 "get_auth_token_%s" % email))
375 "get_auth_token_%s" % email))
376 return q.scalar()
376 return q.scalar()
377
377
378 def update_lastlogin(self):
378 def update_lastlogin(self):
379 """Update user lastlogin"""
379 """Update user lastlogin"""
380 self.last_login = datetime.datetime.now()
380 self.last_login = datetime.datetime.now()
381 Session.add(self)
381 Session.add(self)
382 log.debug('updated user %s lastlogin' % self.username)
382 log.debug('updated user %s lastlogin', self.username)
383
383
384 def __json__(self):
384 def __json__(self):
385 return dict(
385 return dict(
386 user_id=self.user_id,
386 user_id=self.user_id,
387 first_name=self.name,
387 first_name=self.name,
388 last_name=self.lastname,
388 last_name=self.lastname,
389 email=self.email,
389 email=self.email,
390 full_name=self.full_name,
390 full_name=self.full_name,
391 full_name_or_username=self.full_name_or_username,
391 full_name_or_username=self.full_name_or_username,
392 short_contact=self.short_contact,
392 short_contact=self.short_contact,
393 full_contact=self.full_contact
393 full_contact=self.full_contact
394 )
394 )
395
395
396
396
397 class UserLog(Base, BaseModel):
397 class UserLog(Base, BaseModel):
398 __tablename__ = 'user_logs'
398 __tablename__ = 'user_logs'
399 __table_args__ = (
399 __table_args__ = (
400 {'extend_existing': True, 'mysql_engine':'InnoDB',
400 {'extend_existing': True, 'mysql_engine':'InnoDB',
401 'mysql_charset': 'utf8'},
401 'mysql_charset': 'utf8'},
402 )
402 )
403 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
403 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
404 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
404 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
405 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
405 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
406 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
406 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
407 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
407 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
408 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
408 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
409 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
409 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
410
410
411 @property
411 @property
412 def action_as_day(self):
412 def action_as_day(self):
413 return datetime.date(*self.action_date.timetuple()[:3])
413 return datetime.date(*self.action_date.timetuple()[:3])
414
414
415 user = relationship('User')
415 user = relationship('User')
416 repository = relationship('Repository', cascade='')
416 repository = relationship('Repository', cascade='')
417
417
418
418
419 class UserGroup(Base, BaseModel):
419 class UserGroup(Base, BaseModel):
420 __tablename__ = 'users_groups'
420 __tablename__ = 'users_groups'
421 __table_args__ = (
421 __table_args__ = (
422 {'extend_existing': True, 'mysql_engine':'InnoDB',
422 {'extend_existing': True, 'mysql_engine':'InnoDB',
423 'mysql_charset': 'utf8'},
423 'mysql_charset': 'utf8'},
424 )
424 )
425
425
426 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
426 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
427 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
427 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
428 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
428 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
429
429
430 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
430 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
431 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
431 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
432 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
432 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
433
433
434 def __unicode__(self):
434 def __unicode__(self):
435 return u'<userGroup(%s)>' % (self.users_group_name)
435 return u'<userGroup(%s)>' % (self.users_group_name)
436
436
437 @classmethod
437 @classmethod
438 def get_by_group_name(cls, group_name, cache=False,
438 def get_by_group_name(cls, group_name, cache=False,
439 case_insensitive=False):
439 case_insensitive=False):
440 if case_insensitive:
440 if case_insensitive:
441 q = cls.query().filter(cls.users_group_name.ilike(group_name))
441 q = cls.query().filter(cls.users_group_name.ilike(group_name))
442 else:
442 else:
443 q = cls.query().filter(cls.users_group_name == group_name)
443 q = cls.query().filter(cls.users_group_name == group_name)
444 if cache:
444 if cache:
445 q = q.options(FromCache(
445 q = q.options(FromCache(
446 "sql_cache_short",
446 "sql_cache_short",
447 "get_user_%s" % _hash_key(group_name)
447 "get_user_%s" % _hash_key(group_name)
448 )
448 )
449 )
449 )
450 return q.scalar()
450 return q.scalar()
451
451
452 @classmethod
452 @classmethod
453 def get(cls, users_group_id, cache=False):
453 def get(cls, users_group_id, cache=False):
454 users_group = cls.query()
454 users_group = cls.query()
455 if cache:
455 if cache:
456 users_group = users_group.options(FromCache("sql_cache_short",
456 users_group = users_group.options(FromCache("sql_cache_short",
457 "get_users_group_%s" % users_group_id))
457 "get_users_group_%s" % users_group_id))
458 return users_group.get(users_group_id)
458 return users_group.get(users_group_id)
459
459
460
460
461 class UserGroupMember(Base, BaseModel):
461 class UserGroupMember(Base, BaseModel):
462 __tablename__ = 'users_groups_members'
462 __tablename__ = 'users_groups_members'
463 __table_args__ = (
463 __table_args__ = (
464 {'extend_existing': True, 'mysql_engine':'InnoDB',
464 {'extend_existing': True, 'mysql_engine':'InnoDB',
465 'mysql_charset': 'utf8'},
465 'mysql_charset': 'utf8'},
466 )
466 )
467
467
468 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
468 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
469 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
469 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
470 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
470 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
471
471
472 user = relationship('User', lazy='joined')
472 user = relationship('User', lazy='joined')
473 users_group = relationship('UserGroup')
473 users_group = relationship('UserGroup')
474
474
475 def __init__(self, gr_id='', u_id=''):
475 def __init__(self, gr_id='', u_id=''):
476 self.users_group_id = gr_id
476 self.users_group_id = gr_id
477 self.user_id = u_id
477 self.user_id = u_id
478
478
479
479
480 class Repository(Base, BaseModel):
480 class Repository(Base, BaseModel):
481 __tablename__ = 'repositories'
481 __tablename__ = 'repositories'
482 __table_args__ = (
482 __table_args__ = (
483 UniqueConstraint('repo_name'),
483 UniqueConstraint('repo_name'),
484 {'extend_existing': True, 'mysql_engine':'InnoDB',
484 {'extend_existing': True, 'mysql_engine':'InnoDB',
485 'mysql_charset': 'utf8'},
485 'mysql_charset': 'utf8'},
486 )
486 )
487
487
488 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
488 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
489 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
489 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
490 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
490 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
491 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
491 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
493 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
493 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
494 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
494 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
495 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
495 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
496 description = Column("description", String(10000), nullable=True, unique=None, default=None)
496 description = Column("description", String(10000), nullable=True, unique=None, default=None)
497 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
497 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
498
498
499 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
499 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
500 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
500 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
501
501
502 user = relationship('User')
502 user = relationship('User')
503 fork = relationship('Repository', remote_side=repo_id)
503 fork = relationship('Repository', remote_side=repo_id)
504 group = relationship('RepoGroup')
504 group = relationship('RepoGroup')
505 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
505 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
506 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
506 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
507 stats = relationship('Statistics', cascade='all', uselist=False)
507 stats = relationship('Statistics', cascade='all', uselist=False)
508
508
509 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
509 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
510
510
511 logs = relationship('UserLog')
511 logs = relationship('UserLog')
512
512
513 def __unicode__(self):
513 def __unicode__(self):
514 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
514 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
515 self.repo_name)
515 self.repo_name)
516
516
517 @classmethod
517 @classmethod
518 def url_sep(cls):
518 def url_sep(cls):
519 return '/'
519 return '/'
520
520
521 @classmethod
521 @classmethod
522 def get_by_repo_name(cls, repo_name):
522 def get_by_repo_name(cls, repo_name):
523 q = Session.query(cls).filter(cls.repo_name == repo_name)
523 q = Session.query(cls).filter(cls.repo_name == repo_name)
524 q = q.options(joinedload(Repository.fork))\
524 q = q.options(joinedload(Repository.fork))\
525 .options(joinedload(Repository.user))\
525 .options(joinedload(Repository.user))\
526 .options(joinedload(Repository.group))
526 .options(joinedload(Repository.group))
527 return q.scalar()
527 return q.scalar()
528
528
529 @classmethod
529 @classmethod
530 def get_repo_forks(cls, repo_id):
530 def get_repo_forks(cls, repo_id):
531 return cls.query().filter(Repository.fork_id == repo_id)
531 return cls.query().filter(Repository.fork_id == repo_id)
532
532
533 @classmethod
533 @classmethod
534 def base_path(cls):
534 def base_path(cls):
535 """
535 """
536 Returns base path when all repos are stored
536 Returns base path when all repos are stored
537
537
538 :param cls:
538 :param cls:
539 """
539 """
540 q = Session.query(RhodeCodeUi)\
540 q = Session.query(RhodeCodeUi)\
541 .filter(RhodeCodeUi.ui_key == cls.url_sep())
541 .filter(RhodeCodeUi.ui_key == cls.url_sep())
542 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
542 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
543 return q.one().ui_value
543 return q.one().ui_value
544
544
545 @property
545 @property
546 def just_name(self):
546 def just_name(self):
547 return self.repo_name.split(Repository.url_sep())[-1]
547 return self.repo_name.split(Repository.url_sep())[-1]
548
548
549 @property
549 @property
550 def groups_with_parents(self):
550 def groups_with_parents(self):
551 groups = []
551 groups = []
552 if self.group is None:
552 if self.group is None:
553 return groups
553 return groups
554
554
555 cur_gr = self.group
555 cur_gr = self.group
556 groups.insert(0, cur_gr)
556 groups.insert(0, cur_gr)
557 while 1:
557 while 1:
558 gr = getattr(cur_gr, 'parent_group', None)
558 gr = getattr(cur_gr, 'parent_group', None)
559 cur_gr = cur_gr.parent_group
559 cur_gr = cur_gr.parent_group
560 if gr is None:
560 if gr is None:
561 break
561 break
562 groups.insert(0, gr)
562 groups.insert(0, gr)
563
563
564 return groups
564 return groups
565
565
566 @property
566 @property
567 def groups_and_repo(self):
567 def groups_and_repo(self):
568 return self.groups_with_parents, self.just_name
568 return self.groups_with_parents, self.just_name
569
569
570 @LazyProperty
570 @LazyProperty
571 def repo_path(self):
571 def repo_path(self):
572 """
572 """
573 Returns base full path for that repository means where it actually
573 Returns base full path for that repository means where it actually
574 exists on a filesystem
574 exists on a filesystem
575 """
575 """
576 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
576 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
577 Repository.url_sep())
577 Repository.url_sep())
578 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
578 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
579 return q.one().ui_value
579 return q.one().ui_value
580
580
581 @property
581 @property
582 def repo_full_path(self):
582 def repo_full_path(self):
583 p = [self.repo_path]
583 p = [self.repo_path]
584 # we need to split the name by / since this is how we store the
584 # we need to split the name by / since this is how we store the
585 # names in the database, but that eventually needs to be converted
585 # names in the database, but that eventually needs to be converted
586 # into a valid system path
586 # into a valid system path
587 p += self.repo_name.split(Repository.url_sep())
587 p += self.repo_name.split(Repository.url_sep())
588 return os.path.join(*p)
588 return os.path.join(*p)
589
589
590 def get_new_name(self, repo_name):
590 def get_new_name(self, repo_name):
591 """
591 """
592 returns new full repository name based on assigned group and new new
592 returns new full repository name based on assigned group and new new
593
593
594 :param group_name:
594 :param group_name:
595 """
595 """
596 path_prefix = self.group.full_path_splitted if self.group else []
596 path_prefix = self.group.full_path_splitted if self.group else []
597 return Repository.url_sep().join(path_prefix + [repo_name])
597 return Repository.url_sep().join(path_prefix + [repo_name])
598
598
599 @property
599 @property
600 def _config(self):
600 def _config(self):
601 """
601 """
602 Returns db based config object.
602 Returns db based config object.
603 """
603 """
604 from rhodecode.lib.utils import make_db_config
604 from rhodecode.lib.utils import make_db_config
605 return make_db_config(clear_session=False)
605 return make_db_config(clear_session=False)
606
606
607 @classmethod
607 @classmethod
608 def is_valid(cls, repo_name):
608 def is_valid(cls, repo_name):
609 """
609 """
610 returns True if given repo name is a valid filesystem repository
610 returns True if given repo name is a valid filesystem repository
611
611
612 :param cls:
612 :param cls:
613 :param repo_name:
613 :param repo_name:
614 """
614 """
615 from rhodecode.lib.utils import is_valid_repo
615 from rhodecode.lib.utils import is_valid_repo
616
616
617 return is_valid_repo(repo_name, cls.base_path())
617 return is_valid_repo(repo_name, cls.base_path())
618
618
619 #==========================================================================
619 #==========================================================================
620 # SCM PROPERTIES
620 # SCM PROPERTIES
621 #==========================================================================
621 #==========================================================================
622
622
623 def get_commit(self, rev):
623 def get_commit(self, rev):
624 return get_commit_safe(self.scm_instance, rev)
624 return get_commit_safe(self.scm_instance, rev)
625
625
626 @property
626 @property
627 def tip(self):
627 def tip(self):
628 return self.get_commit('tip')
628 return self.get_commit('tip')
629
629
630 @property
630 @property
631 def author(self):
631 def author(self):
632 return self.tip.author
632 return self.tip.author
633
633
634 @property
634 @property
635 def last_change(self):
635 def last_change(self):
636 return self.scm_instance.last_change
636 return self.scm_instance.last_change
637
637
638 def comments(self, revisions=None):
638 def comments(self, revisions=None):
639 """
639 """
640 Returns comments for this repository grouped by revisions
640 Returns comments for this repository grouped by revisions
641
641
642 :param revisions: filter query by revisions only
642 :param revisions: filter query by revisions only
643 """
643 """
644 cmts = ChangesetComment.query()\
644 cmts = ChangesetComment.query()\
645 .filter(ChangesetComment.repo == self)
645 .filter(ChangesetComment.repo == self)
646 if revisions:
646 if revisions:
647 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
647 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
648 grouped = defaultdict(list)
648 grouped = defaultdict(list)
649 for cmt in cmts.all():
649 for cmt in cmts.all():
650 grouped[cmt.revision].append(cmt)
650 grouped[cmt.revision].append(cmt)
651 return grouped
651 return grouped
652
652
653 #==========================================================================
653 #==========================================================================
654 # SCM CACHE INSTANCE
654 # SCM CACHE INSTANCE
655 #==========================================================================
655 #==========================================================================
656
656
657 @property
657 @property
658 def invalidate(self):
658 def invalidate(self):
659 return CacheInvalidation.invalidate(self.repo_name)
659 return CacheInvalidation.invalidate(self.repo_name)
660
660
661 def set_invalidate(self):
661 def set_invalidate(self):
662 """
662 """
663 set a cache for invalidation for this instance
663 set a cache for invalidation for this instance
664 """
664 """
665 CacheInvalidation.set_invalidate(self.repo_name)
665 CacheInvalidation.set_invalidate(self.repo_name)
666
666
667 @LazyProperty
667 @LazyProperty
668 def scm_instance(self):
668 def scm_instance(self):
669 return self.__get_instance()
669 return self.__get_instance()
670
670
671 @property
671 @property
672 def scm_instance_cached(self):
672 def scm_instance_cached(self):
673 return self.__get_instance()
673 return self.__get_instance()
674
674
675 def __get_instance(self):
675 def __get_instance(self):
676 repo_full_path = self.repo_full_path
676 repo_full_path = self.repo_full_path
677 try:
677 try:
678 alias = get_scm(repo_full_path)[0]
678 alias = get_scm(repo_full_path)[0]
679 log.debug('Creating instance of %s repository' % alias)
679 log.debug('Creating instance of %s repository', alias)
680 backend = get_backend(alias)
680 backend = get_backend(alias)
681 except VCSError:
681 except VCSError:
682 log.error(traceback.format_exc())
682 log.error(traceback.format_exc())
683 log.error('Perhaps this repository is in db and not in '
683 log.error('Perhaps this repository is in db and not in '
684 'filesystem run rescan repositories with '
684 'filesystem run rescan repositories with '
685 '"destroy old data " option from admin panel')
685 '"destroy old data " option from admin panel')
686 return
686 return
687
687
688 if alias == 'hg':
688 if alias == 'hg':
689
689
690 repo = backend(safe_str(repo_full_path), create=False,
690 repo = backend(safe_str(repo_full_path), create=False,
691 config=self._config)
691 config=self._config)
692 else:
692 else:
693 repo = backend(repo_full_path, create=False)
693 repo = backend(repo_full_path, create=False)
694
694
695 return repo
695 return repo
696
696
697
697
698 class RepoGroup(Base, BaseModel):
698 class RepoGroup(Base, BaseModel):
699 __tablename__ = 'groups'
699 __tablename__ = 'groups'
700 __table_args__ = (
700 __table_args__ = (
701 UniqueConstraint('group_name', 'group_parent_id'),
701 UniqueConstraint('group_name', 'group_parent_id'),
702 CheckConstraint('group_id != group_parent_id'),
702 CheckConstraint('group_id != group_parent_id'),
703 {'extend_existing': True, 'mysql_engine':'InnoDB',
703 {'extend_existing': True, 'mysql_engine':'InnoDB',
704 'mysql_charset': 'utf8'},
704 'mysql_charset': 'utf8'},
705 )
705 )
706 __mapper_args__ = {'order_by': 'group_name'}
706 __mapper_args__ = {'order_by': 'group_name'}
707
707
708 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
708 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
709 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
709 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
710 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
710 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
711 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
711 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
712
712
713 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
713 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
714 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
714 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
715
715
716 parent_group = relationship('RepoGroup', remote_side=group_id)
716 parent_group = relationship('RepoGroup', remote_side=group_id)
717
717
718 def __init__(self, group_name='', parent_group=None):
718 def __init__(self, group_name='', parent_group=None):
719 self.group_name = group_name
719 self.group_name = group_name
720 self.parent_group = parent_group
720 self.parent_group = parent_group
721
721
722 def __unicode__(self):
722 def __unicode__(self):
723 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
723 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
724 self.group_name)
724 self.group_name)
725
725
726 @classmethod
726 @classmethod
727 def url_sep(cls):
727 def url_sep(cls):
728 return '/'
728 return '/'
729
729
730 @classmethod
730 @classmethod
731 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
731 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
732 if case_insensitive:
732 if case_insensitive:
733 gr = cls.query()\
733 gr = cls.query()\
734 .filter(cls.group_name.ilike(group_name))
734 .filter(cls.group_name.ilike(group_name))
735 else:
735 else:
736 gr = cls.query()\
736 gr = cls.query()\
737 .filter(cls.group_name == group_name)
737 .filter(cls.group_name == group_name)
738 if cache:
738 if cache:
739 gr = gr.options(FromCache(
739 gr = gr.options(FromCache(
740 "sql_cache_short",
740 "sql_cache_short",
741 "get_group_%s" % _hash_key(group_name)
741 "get_group_%s" % _hash_key(group_name)
742 )
742 )
743 )
743 )
744 return gr.scalar()
744 return gr.scalar()
745
745
746 @property
746 @property
747 def parents(self):
747 def parents(self):
748 parents_recursion_limit = 5
748 parents_recursion_limit = 5
749 groups = []
749 groups = []
750 if self.parent_group is None:
750 if self.parent_group is None:
751 return groups
751 return groups
752 cur_gr = self.parent_group
752 cur_gr = self.parent_group
753 groups.insert(0, cur_gr)
753 groups.insert(0, cur_gr)
754 cnt = 0
754 cnt = 0
755 while 1:
755 while 1:
756 cnt += 1
756 cnt += 1
757 gr = getattr(cur_gr, 'parent_group', None)
757 gr = getattr(cur_gr, 'parent_group', None)
758 cur_gr = cur_gr.parent_group
758 cur_gr = cur_gr.parent_group
759 if gr is None:
759 if gr is None:
760 break
760 break
761 if cnt == parents_recursion_limit:
761 if cnt == parents_recursion_limit:
762 # this will prevent accidental infinit loops
762 # this will prevent accidental infinit loops
763 log.error('group nested more than %s' %
763 log.error('group nested more than %s', parents_recursion_limit)
764 parents_recursion_limit)
765 break
764 break
766
765
767 groups.insert(0, gr)
766 groups.insert(0, gr)
768 return groups
767 return groups
769
768
770 @property
769 @property
771 def children(self):
770 def children(self):
772 return RepoGroup.query().filter(RepoGroup.parent_group == self)
771 return RepoGroup.query().filter(RepoGroup.parent_group == self)
773
772
774 @property
773 @property
775 def name(self):
774 def name(self):
776 return self.group_name.split(RepoGroup.url_sep())[-1]
775 return self.group_name.split(RepoGroup.url_sep())[-1]
777
776
778 @property
777 @property
779 def full_path(self):
778 def full_path(self):
780 return self.group_name
779 return self.group_name
781
780
782 @property
781 @property
783 def full_path_splitted(self):
782 def full_path_splitted(self):
784 return self.group_name.split(RepoGroup.url_sep())
783 return self.group_name.split(RepoGroup.url_sep())
785
784
786 @property
785 @property
787 def repositories(self):
786 def repositories(self):
788 return Repository.query()\
787 return Repository.query()\
789 .filter(Repository.group == self)\
788 .filter(Repository.group == self)\
790 .order_by(Repository.repo_name)
789 .order_by(Repository.repo_name)
791
790
792 @property
791 @property
793 def repositories_recursive_count(self):
792 def repositories_recursive_count(self):
794 cnt = self.repositories.count()
793 cnt = self.repositories.count()
795
794
796 def children_count(group):
795 def children_count(group):
797 cnt = 0
796 cnt = 0
798 for child in group.children:
797 for child in group.children:
799 cnt += child.repositories.count()
798 cnt += child.repositories.count()
800 cnt += children_count(child)
799 cnt += children_count(child)
801 return cnt
800 return cnt
802
801
803 return cnt + children_count(self)
802 return cnt + children_count(self)
804
803
805 def get_new_name(self, group_name):
804 def get_new_name(self, group_name):
806 """
805 """
807 returns new full group name based on parent and new name
806 returns new full group name based on parent and new name
808
807
809 :param group_name:
808 :param group_name:
810 """
809 """
811 path_prefix = (self.parent_group.full_path_splitted if
810 path_prefix = (self.parent_group.full_path_splitted if
812 self.parent_group else [])
811 self.parent_group else [])
813 return RepoGroup.url_sep().join(path_prefix + [group_name])
812 return RepoGroup.url_sep().join(path_prefix + [group_name])
814
813
815
814
816 class Permission(Base, BaseModel):
815 class Permission(Base, BaseModel):
817 __tablename__ = 'permissions'
816 __tablename__ = 'permissions'
818 __table_args__ = (
817 __table_args__ = (
819 {'extend_existing': True, 'mysql_engine':'InnoDB',
818 {'extend_existing': True, 'mysql_engine':'InnoDB',
820 'mysql_charset': 'utf8'},
819 'mysql_charset': 'utf8'},
821 )
820 )
822 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
821 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
823 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
822 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
824 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
823 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
825
824
826 def __unicode__(self):
825 def __unicode__(self):
827 return u"<%s('%s:%s')>" % (
826 return u"<%s('%s:%s')>" % (
828 self.__class__.__name__, self.permission_id, self.permission_name
827 self.__class__.__name__, self.permission_id, self.permission_name
829 )
828 )
830
829
831 @classmethod
830 @classmethod
832 def get_by_key(cls, key):
831 def get_by_key(cls, key):
833 return cls.query().filter(cls.permission_name == key).scalar()
832 return cls.query().filter(cls.permission_name == key).scalar()
834
833
835 @classmethod
834 @classmethod
836 def get_default_repo_perms(cls, default_user_id):
835 def get_default_repo_perms(cls, default_user_id):
837 q = Session.query(UserRepoToPerm, Repository, cls)\
836 q = Session.query(UserRepoToPerm, Repository, cls)\
838 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
837 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
839 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
838 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
840 .filter(UserRepoToPerm.user_id == default_user_id)
839 .filter(UserRepoToPerm.user_id == default_user_id)
841
840
842 return q.all()
841 return q.all()
843
842
844 @classmethod
843 @classmethod
845 def get_default_group_perms(cls, default_user_id):
844 def get_default_group_perms(cls, default_user_id):
846 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
845 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
847 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
846 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
848 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
847 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
849 .filter(UserRepoGroupToPerm.user_id == default_user_id)
848 .filter(UserRepoGroupToPerm.user_id == default_user_id)
850
849
851 return q.all()
850 return q.all()
852
851
853
852
854 class UserRepoToPerm(Base, BaseModel):
853 class UserRepoToPerm(Base, BaseModel):
855 __tablename__ = 'repo_to_perm'
854 __tablename__ = 'repo_to_perm'
856 __table_args__ = (
855 __table_args__ = (
857 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
856 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
858 {'extend_existing': True, 'mysql_engine':'InnoDB',
857 {'extend_existing': True, 'mysql_engine':'InnoDB',
859 'mysql_charset': 'utf8'}
858 'mysql_charset': 'utf8'}
860 )
859 )
861 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
860 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
862 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
861 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
863 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
862 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
864 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
863 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
865
864
866 user = relationship('User')
865 user = relationship('User')
867 repository = relationship('Repository')
866 repository = relationship('Repository')
868 permission = relationship('Permission')
867 permission = relationship('Permission')
869
868
870 @classmethod
869 @classmethod
871 def create(cls, user, repository, permission):
870 def create(cls, user, repository, permission):
872 n = cls()
871 n = cls()
873 n.user = user
872 n.user = user
874 n.repository = repository
873 n.repository = repository
875 n.permission = permission
874 n.permission = permission
876 Session.add(n)
875 Session.add(n)
877 return n
876 return n
878
877
879 def __unicode__(self):
878 def __unicode__(self):
880 return u'<user:%s => %s >' % (self.user, self.repository)
879 return u'<user:%s => %s >' % (self.user, self.repository)
881
880
882
881
883 class UserToPerm(Base, BaseModel):
882 class UserToPerm(Base, BaseModel):
884 __tablename__ = 'user_to_perm'
883 __tablename__ = 'user_to_perm'
885 __table_args__ = (
884 __table_args__ = (
886 UniqueConstraint('user_id', 'permission_id'),
885 UniqueConstraint('user_id', 'permission_id'),
887 {'extend_existing': True, 'mysql_engine':'InnoDB',
886 {'extend_existing': True, 'mysql_engine':'InnoDB',
888 'mysql_charset': 'utf8'}
887 'mysql_charset': 'utf8'}
889 )
888 )
890 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
889 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
891 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
890 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
892 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
891 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
893
892
894 user = relationship('User')
893 user = relationship('User')
895 permission = relationship('Permission', lazy='joined')
894 permission = relationship('Permission', lazy='joined')
896
895
897
896
898 class UserGroupRepoToPerm(Base, BaseModel):
897 class UserGroupRepoToPerm(Base, BaseModel):
899 __tablename__ = 'users_group_repo_to_perm'
898 __tablename__ = 'users_group_repo_to_perm'
900 __table_args__ = (
899 __table_args__ = (
901 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
900 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
902 {'extend_existing': True, 'mysql_engine':'InnoDB',
901 {'extend_existing': True, 'mysql_engine':'InnoDB',
903 'mysql_charset': 'utf8'}
902 'mysql_charset': 'utf8'}
904 )
903 )
905 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
904 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
906 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
905 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
907 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
906 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
908 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
907 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
909
908
910 users_group = relationship('UserGroup')
909 users_group = relationship('UserGroup')
911 permission = relationship('Permission')
910 permission = relationship('Permission')
912 repository = relationship('Repository')
911 repository = relationship('Repository')
913
912
914 @classmethod
913 @classmethod
915 def create(cls, users_group, repository, permission):
914 def create(cls, users_group, repository, permission):
916 n = cls()
915 n = cls()
917 n.users_group = users_group
916 n.users_group = users_group
918 n.repository = repository
917 n.repository = repository
919 n.permission = permission
918 n.permission = permission
920 Session.add(n)
919 Session.add(n)
921 return n
920 return n
922
921
923 def __unicode__(self):
922 def __unicode__(self):
924 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
923 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
925
924
926
925
927 class UserGroupToPerm(Base, BaseModel):
926 class UserGroupToPerm(Base, BaseModel):
928 __tablename__ = 'users_group_to_perm'
927 __tablename__ = 'users_group_to_perm'
929 __table_args__ = (
928 __table_args__ = (
930 UniqueConstraint('users_group_id', 'permission_id',),
929 UniqueConstraint('users_group_id', 'permission_id',),
931 {'extend_existing': True, 'mysql_engine':'InnoDB',
930 {'extend_existing': True, 'mysql_engine':'InnoDB',
932 'mysql_charset': 'utf8'}
931 'mysql_charset': 'utf8'}
933 )
932 )
934 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
933 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
935 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
934 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
936 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
935 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
937
936
938 users_group = relationship('UserGroup')
937 users_group = relationship('UserGroup')
939 permission = relationship('Permission')
938 permission = relationship('Permission')
940
939
941
940
942 class UserRepoGroupToPerm(Base, BaseModel):
941 class UserRepoGroupToPerm(Base, BaseModel):
943 __tablename__ = 'user_repo_group_to_perm'
942 __tablename__ = 'user_repo_group_to_perm'
944 __table_args__ = (
943 __table_args__ = (
945 UniqueConstraint('user_id', 'group_id', 'permission_id'),
944 UniqueConstraint('user_id', 'group_id', 'permission_id'),
946 {'extend_existing': True, 'mysql_engine':'InnoDB',
945 {'extend_existing': True, 'mysql_engine':'InnoDB',
947 'mysql_charset': 'utf8'}
946 'mysql_charset': 'utf8'}
948 )
947 )
949
948
950 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
949 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
951 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
950 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
952 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
951 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
953 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
952 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
954
953
955 user = relationship('User')
954 user = relationship('User')
956 group = relationship('RepoGroup')
955 group = relationship('RepoGroup')
957 permission = relationship('Permission')
956 permission = relationship('Permission')
958
957
959
958
960 class UserGroupRepoGroupToPerm(Base, BaseModel):
959 class UserGroupRepoGroupToPerm(Base, BaseModel):
961 __tablename__ = 'users_group_repo_group_to_perm'
960 __tablename__ = 'users_group_repo_group_to_perm'
962 __table_args__ = (
961 __table_args__ = (
963 UniqueConstraint('users_group_id', 'group_id'),
962 UniqueConstraint('users_group_id', 'group_id'),
964 {'extend_existing': True, 'mysql_engine':'InnoDB',
963 {'extend_existing': True, 'mysql_engine':'InnoDB',
965 'mysql_charset': 'utf8'}
964 'mysql_charset': 'utf8'}
966 )
965 )
967
966
968 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)
967 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)
969 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
968 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
970 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
969 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
971 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
970 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
972
971
973 users_group = relationship('UserGroup')
972 users_group = relationship('UserGroup')
974 permission = relationship('Permission')
973 permission = relationship('Permission')
975 group = relationship('RepoGroup')
974 group = relationship('RepoGroup')
976
975
977
976
978 class Statistics(Base, BaseModel):
977 class Statistics(Base, BaseModel):
979 __tablename__ = 'statistics'
978 __tablename__ = 'statistics'
980 __table_args__ = (
979 __table_args__ = (
981 UniqueConstraint('repository_id'),
980 UniqueConstraint('repository_id'),
982 {'extend_existing': True, 'mysql_engine':'InnoDB',
981 {'extend_existing': True, 'mysql_engine':'InnoDB',
983 'mysql_charset': 'utf8'}
982 'mysql_charset': 'utf8'}
984 )
983 )
985 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
984 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
986 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
985 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
987 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
986 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
988 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
987 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
989 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
988 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
990 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
989 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
991
990
992 repository = relationship('Repository', single_parent=True)
991 repository = relationship('Repository', single_parent=True)
993
992
994
993
995 class UserFollowing(Base, BaseModel):
994 class UserFollowing(Base, BaseModel):
996 __tablename__ = 'user_followings'
995 __tablename__ = 'user_followings'
997 __table_args__ = (
996 __table_args__ = (
998 UniqueConstraint('user_id', 'follows_repository_id'),
997 UniqueConstraint('user_id', 'follows_repository_id'),
999 UniqueConstraint('user_id', 'follows_user_id'),
998 UniqueConstraint('user_id', 'follows_user_id'),
1000 {'extend_existing': True, 'mysql_engine':'InnoDB',
999 {'extend_existing': True, 'mysql_engine':'InnoDB',
1001 'mysql_charset': 'utf8'}
1000 'mysql_charset': 'utf8'}
1002 )
1001 )
1003
1002
1004 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1003 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1004 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1006 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1005 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1007 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1006 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1008 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1007 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1009
1008
1010 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1009 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1011
1010
1012 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1011 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1013 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1012 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1014
1013
1015 @classmethod
1014 @classmethod
1016 def get_repo_followers(cls, repo_id):
1015 def get_repo_followers(cls, repo_id):
1017 return cls.query().filter(cls.follows_repo_id == repo_id)
1016 return cls.query().filter(cls.follows_repo_id == repo_id)
1018
1017
1019
1018
1020 class CacheInvalidation(Base, BaseModel):
1019 class CacheInvalidation(Base, BaseModel):
1021 __tablename__ = 'cache_invalidation'
1020 __tablename__ = 'cache_invalidation'
1022 __table_args__ = (
1021 __table_args__ = (
1023 UniqueConstraint('cache_key'),
1022 UniqueConstraint('cache_key'),
1024 {'extend_existing': True, 'mysql_engine':'InnoDB',
1023 {'extend_existing': True, 'mysql_engine':'InnoDB',
1025 'mysql_charset': 'utf8'},
1024 'mysql_charset': 'utf8'},
1026 )
1025 )
1027 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1026 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1028 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
1027 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
1029 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
1028 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
1030 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1029 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1031
1030
1032 def __init__(self, cache_key, cache_args=''):
1031 def __init__(self, cache_key, cache_args=''):
1033 self.cache_key = cache_key
1032 self.cache_key = cache_key
1034 self.cache_args = cache_args
1033 self.cache_args = cache_args
1035 self.cache_active = False
1034 self.cache_active = False
1036
1035
1037 def __unicode__(self):
1036 def __unicode__(self):
1038 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1037 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1039 self.cache_id, self.cache_key)
1038 self.cache_id, self.cache_key)
1040
1039
1041 @classmethod
1040 @classmethod
1042 def _get_key(cls, key):
1041 def _get_key(cls, key):
1043 """
1042 """
1044 Wrapper for generating a key, together with a prefix
1043 Wrapper for generating a key, together with a prefix
1045
1044
1046 :param key:
1045 :param key:
1047 """
1046 """
1048 import rhodecode
1047 import rhodecode
1049 prefix = ''
1048 prefix = ''
1050 iid = rhodecode.CONFIG.get('instance_id')
1049 iid = rhodecode.CONFIG.get('instance_id')
1051 if iid:
1050 if iid:
1052 prefix = iid
1051 prefix = iid
1053 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1052 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1054
1053
1055 @classmethod
1054 @classmethod
1056 def get_by_key(cls, key):
1055 def get_by_key(cls, key):
1057 return cls.query().filter(cls.cache_key == key).scalar()
1056 return cls.query().filter(cls.cache_key == key).scalar()
1058
1057
1059 @classmethod
1058 @classmethod
1060 def _get_or_create_key(cls, key, prefix, org_key):
1059 def _get_or_create_key(cls, key, prefix, org_key):
1061 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1060 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1062 if not inv_obj:
1061 if not inv_obj:
1063 try:
1062 try:
1064 inv_obj = CacheInvalidation(key, org_key)
1063 inv_obj = CacheInvalidation(key, org_key)
1065 Session.add(inv_obj)
1064 Session.add(inv_obj)
1066 Session.commit()
1065 Session.commit()
1067 except Exception:
1066 except Exception:
1068 log.error(traceback.format_exc())
1067 log.error(traceback.format_exc())
1069 Session.rollback()
1068 Session.rollback()
1070 return inv_obj
1069 return inv_obj
1071
1070
1072 @classmethod
1071 @classmethod
1073 def invalidate(cls, key):
1072 def invalidate(cls, key):
1074 """
1073 """
1075 Returns Invalidation object if this given key should be invalidated
1074 Returns Invalidation object if this given key should be invalidated
1076 None otherwise. `cache_active = False` means that this cache
1075 None otherwise. `cache_active = False` means that this cache
1077 state is not valid and needs to be invalidated
1076 state is not valid and needs to be invalidated
1078
1077
1079 :param key:
1078 :param key:
1080 """
1079 """
1081
1080
1082 key, _prefix, _org_key = cls._get_key(key)
1081 key, _prefix, _org_key = cls._get_key(key)
1083 inv = cls._get_or_create_key(key, _prefix, _org_key)
1082 inv = cls._get_or_create_key(key, _prefix, _org_key)
1084
1083
1085 if inv and inv.cache_active is False:
1084 if inv and inv.cache_active is False:
1086 return inv
1085 return inv
1087
1086
1088 @classmethod
1087 @classmethod
1089 def set_invalidate(cls, key):
1088 def set_invalidate(cls, key):
1090 """
1089 """
1091 Mark this Cache key for invalidation
1090 Mark this Cache key for invalidation
1092
1091
1093 :param key:
1092 :param key:
1094 """
1093 """
1095
1094
1096 key, _prefix, _org_key = cls._get_key(key)
1095 key, _prefix, _org_key = cls._get_key(key)
1097 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1096 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1098 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1097 log.debug('marking %s key[s] %s for invalidation', len(inv_objs), _org_key)
1099 _org_key))
1100 try:
1098 try:
1101 for inv_obj in inv_objs:
1099 for inv_obj in inv_objs:
1102 if inv_obj:
1100 if inv_obj:
1103 inv_obj.cache_active = False
1101 inv_obj.cache_active = False
1104
1102
1105 Session.add(inv_obj)
1103 Session.add(inv_obj)
1106 Session.commit()
1104 Session.commit()
1107 except Exception:
1105 except Exception:
1108 log.error(traceback.format_exc())
1106 log.error(traceback.format_exc())
1109 Session.rollback()
1107 Session.rollback()
1110
1108
1111 @classmethod
1109 @classmethod
1112 def set_valid(cls, key):
1110 def set_valid(cls, key):
1113 """
1111 """
1114 Mark this cache key as active and currently cached
1112 Mark this cache key as active and currently cached
1115
1113
1116 :param key:
1114 :param key:
1117 """
1115 """
1118 inv_obj = cls.get_by_key(key)
1116 inv_obj = cls.get_by_key(key)
1119 inv_obj.cache_active = True
1117 inv_obj.cache_active = True
1120 Session.add(inv_obj)
1118 Session.add(inv_obj)
1121 Session.commit()
1119 Session.commit()
1122
1120
1123
1121
1124 class ChangesetComment(Base, BaseModel):
1122 class ChangesetComment(Base, BaseModel):
1125 __tablename__ = 'changeset_comments'
1123 __tablename__ = 'changeset_comments'
1126 __table_args__ = (
1124 __table_args__ = (
1127 {'extend_existing': True, 'mysql_engine':'InnoDB',
1125 {'extend_existing': True, 'mysql_engine':'InnoDB',
1128 'mysql_charset': 'utf8'},
1126 'mysql_charset': 'utf8'},
1129 )
1127 )
1130 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1128 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1131 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1129 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1132 revision = Column('revision', String(40), nullable=False)
1130 revision = Column('revision', String(40), nullable=False)
1133 line_no = Column('line_no', Unicode(10), nullable=True)
1131 line_no = Column('line_no', Unicode(10), nullable=True)
1134 f_path = Column('f_path', Unicode(1000), nullable=True)
1132 f_path = Column('f_path', Unicode(1000), nullable=True)
1135 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1133 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1136 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
1134 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
1137 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1135 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1138
1136
1139 author = relationship('User', lazy='joined')
1137 author = relationship('User', lazy='joined')
1140 repo = relationship('Repository')
1138 repo = relationship('Repository')
1141
1139
1142 @classmethod
1140 @classmethod
1143 def get_users(cls, revision):
1141 def get_users(cls, revision):
1144 """
1142 """
1145 Returns user associated with this changesetComment. ie those
1143 Returns user associated with this changesetComment. ie those
1146 who actually commented
1144 who actually commented
1147
1145
1148 :param cls:
1146 :param cls:
1149 :param revision:
1147 :param revision:
1150 """
1148 """
1151 return Session.query(User)\
1149 return Session.query(User)\
1152 .filter(cls.revision == revision)\
1150 .filter(cls.revision == revision)\
1153 .join(ChangesetComment.author).all()
1151 .join(ChangesetComment.author).all()
1154
1152
1155
1153
1156 class Notification(Base, BaseModel):
1154 class Notification(Base, BaseModel):
1157 __tablename__ = 'notifications'
1155 __tablename__ = 'notifications'
1158 __table_args__ = (
1156 __table_args__ = (
1159 {'extend_existing': True, 'mysql_engine':'InnoDB',
1157 {'extend_existing': True, 'mysql_engine':'InnoDB',
1160 'mysql_charset': 'utf8'},
1158 'mysql_charset': 'utf8'},
1161 )
1159 )
1162
1160
1163 TYPE_CHANGESET_COMMENT = u'cs_comment'
1161 TYPE_CHANGESET_COMMENT = u'cs_comment'
1164 TYPE_MESSAGE = u'message'
1162 TYPE_MESSAGE = u'message'
1165 TYPE_MENTION = u'mention'
1163 TYPE_MENTION = u'mention'
1166 TYPE_REGISTRATION = u'registration'
1164 TYPE_REGISTRATION = u'registration'
1167
1165
1168 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1166 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1169 subject = Column('subject', Unicode(512), nullable=True)
1167 subject = Column('subject', Unicode(512), nullable=True)
1170 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
1168 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
1171 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1169 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1172 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1170 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1173 type_ = Column('type', Unicode(256))
1171 type_ = Column('type', Unicode(256))
1174
1172
1175 created_by_user = relationship('User')
1173 created_by_user = relationship('User')
1176 notifications_to_users = relationship('UserNotification', lazy='joined',
1174 notifications_to_users = relationship('UserNotification', lazy='joined',
1177 cascade="all, delete, delete-orphan")
1175 cascade="all, delete, delete-orphan")
1178
1176
1179 @property
1177 @property
1180 def recipients(self):
1178 def recipients(self):
1181 return [x.user for x in UserNotification.query()\
1179 return [x.user for x in UserNotification.query()\
1182 .filter(UserNotification.notification == self).all()]
1180 .filter(UserNotification.notification == self).all()]
1183
1181
1184 @classmethod
1182 @classmethod
1185 def create(cls, created_by, subject, body, recipients, type_=None):
1183 def create(cls, created_by, subject, body, recipients, type_=None):
1186 if type_ is None:
1184 if type_ is None:
1187 type_ = Notification.TYPE_MESSAGE
1185 type_ = Notification.TYPE_MESSAGE
1188
1186
1189 notification = cls()
1187 notification = cls()
1190 notification.created_by_user = created_by
1188 notification.created_by_user = created_by
1191 notification.subject = subject
1189 notification.subject = subject
1192 notification.body = body
1190 notification.body = body
1193 notification.type_ = type_
1191 notification.type_ = type_
1194 notification.created_on = datetime.datetime.now()
1192 notification.created_on = datetime.datetime.now()
1195
1193
1196 for u in recipients:
1194 for u in recipients:
1197 assoc = UserNotification()
1195 assoc = UserNotification()
1198 assoc.notification = notification
1196 assoc.notification = notification
1199 u.notifications.append(assoc)
1197 u.notifications.append(assoc)
1200 Session.add(notification)
1198 Session.add(notification)
1201 return notification
1199 return notification
1202
1200
1203 @property
1201 @property
1204 def description(self):
1202 def description(self):
1205 from rhodecode.model.notification import NotificationModel
1203 from rhodecode.model.notification import NotificationModel
1206 return NotificationModel().make_description(self)
1204 return NotificationModel().make_description(self)
1207
1205
1208
1206
1209 class UserNotification(Base, BaseModel):
1207 class UserNotification(Base, BaseModel):
1210 __tablename__ = 'user_to_notification'
1208 __tablename__ = 'user_to_notification'
1211 __table_args__ = (
1209 __table_args__ = (
1212 UniqueConstraint('user_id', 'notification_id'),
1210 UniqueConstraint('user_id', 'notification_id'),
1213 {'extend_existing': True, 'mysql_engine':'InnoDB',
1211 {'extend_existing': True, 'mysql_engine':'InnoDB',
1214 'mysql_charset': 'utf8'}
1212 'mysql_charset': 'utf8'}
1215 )
1213 )
1216 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1214 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1217 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1215 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1218 read = Column('read', Boolean, default=False)
1216 read = Column('read', Boolean, default=False)
1219 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1217 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1220
1218
1221 user = relationship('User', lazy="joined")
1219 user = relationship('User', lazy="joined")
1222 notification = relationship('Notification', lazy="joined",
1220 notification = relationship('Notification', lazy="joined",
1223 order_by=lambda: Notification.created_on.desc(),)
1221 order_by=lambda: Notification.created_on.desc(),)
1224
1222
1225 def mark_as_read(self):
1223 def mark_as_read(self):
1226 self.read = True
1224 self.read = True
1227 Session.add(self)
1225 Session.add(self)
1228
1226
1229
1227
1230 class DbMigrateVersion(Base, BaseModel):
1228 class DbMigrateVersion(Base, BaseModel):
1231 __tablename__ = 'db_migrate_version'
1229 __tablename__ = 'db_migrate_version'
1232 __table_args__ = (
1230 __table_args__ = (
1233 {'extend_existing': True, 'mysql_engine':'InnoDB',
1231 {'extend_existing': True, 'mysql_engine':'InnoDB',
1234 'mysql_charset': 'utf8'},
1232 'mysql_charset': 'utf8'},
1235 )
1233 )
1236 repository_id = Column('repository_id', String(250), primary_key=True)
1234 repository_id = Column('repository_id', String(250), primary_key=True)
1237 repository_path = Column('repository_path', Text)
1235 repository_path = Column('repository_path', Text)
1238 version = Column('version', Integer)
1236 version = Column('version', Integer)
1239
1237
1240 ## this is migration from 1_4_0, but now it's here to overcome a problem of
1238 ## this is migration from 1_4_0, but now it's here to overcome a problem of
1241 ## attaching a FK to this from 1_3_0 !
1239 ## attaching a FK to this from 1_3_0 !
1242
1240
1243
1241
1244 class PullRequest(Base, BaseModel):
1242 class PullRequest(Base, BaseModel):
1245 __tablename__ = 'pull_requests'
1243 __tablename__ = 'pull_requests'
1246 __table_args__ = (
1244 __table_args__ = (
1247 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1245 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1248 'mysql_charset': 'utf8'},
1246 'mysql_charset': 'utf8'},
1249 )
1247 )
1250
1248
1251 STATUS_NEW = u'new'
1249 STATUS_NEW = u'new'
1252 STATUS_OPEN = u'open'
1250 STATUS_OPEN = u'open'
1253 STATUS_CLOSED = u'closed'
1251 STATUS_CLOSED = u'closed'
1254
1252
1255 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1253 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1256 title = Column('title', Unicode(256), nullable=True)
1254 title = Column('title', Unicode(256), nullable=True)
1257 description = Column('description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
1255 description = Column('description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
1258 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1256 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1259 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1257 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1260 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1258 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1261 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1259 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1262 _revisions = Column('revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) # 500 revisions max
1260 _revisions = Column('revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) # 500 revisions max
1263 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1261 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1264 org_ref = Column('org_ref', Unicode(256), nullable=False)
1262 org_ref = Column('org_ref', Unicode(256), nullable=False)
1265 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1263 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1266 other_ref = Column('other_ref', Unicode(256), nullable=False)
1264 other_ref = Column('other_ref', Unicode(256), nullable=False)
@@ -1,1087 +1,1085 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 time
22 import time
23 import logging
23 import logging
24 import datetime
24 import datetime
25 import traceback
25 import traceback
26 import hashlib
26 import hashlib
27 import collections
27 import collections
28
28
29 from sqlalchemy import *
29 from sqlalchemy import *
30 from sqlalchemy.ext.hybrid import hybrid_property
30 from sqlalchemy.ext.hybrid import hybrid_property
31 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
31 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
32 from sqlalchemy.exc import DatabaseError
32 from sqlalchemy.exc import DatabaseError
33 from beaker.cache import cache_region, region_invalidate
33 from beaker.cache import cache_region, region_invalidate
34 from webob.exc import HTTPNotFound
34 from webob.exc import HTTPNotFound
35
35
36 from rhodecode.translation import _
36 from rhodecode.translation import _
37
37
38 from rhodecode.lib.vcs import get_backend
38 from rhodecode.lib.vcs import get_backend
39 from rhodecode.lib.vcs.utils.helpers import get_scm
39 from rhodecode.lib.vcs.utils.helpers import get_scm
40 from rhodecode.lib.vcs.exceptions import VCSError
40 from rhodecode.lib.vcs.exceptions import VCSError
41 from zope.cachedescriptors.property import Lazy as LazyProperty
41 from zope.cachedescriptors.property import Lazy as LazyProperty
42 from rhodecode.lib.vcs.backends.base import EmptyCommit
42 from rhodecode.lib.vcs.backends.base import EmptyCommit
43
43
44 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, \
44 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, \
45 safe_unicode, remove_suffix, remove_prefix, time_to_datetime
45 safe_unicode, remove_suffix, remove_prefix, time_to_datetime
46 from rhodecode.lib.ext_json import json
46 from rhodecode.lib.ext_json import json
47 from rhodecode.lib.caching_query import FromCache
47 from rhodecode.lib.caching_query import FromCache
48
48
49 from rhodecode.model.meta import Base, Session
49 from rhodecode.model.meta import Base, Session
50
50
51 URL_SEP = '/'
51 URL_SEP = '/'
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54 #==============================================================================
54 #==============================================================================
55 # BASE CLASSES
55 # BASE CLASSES
56 #==============================================================================
56 #==============================================================================
57
57
58 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
58 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
59
59
60
60
61 class BaseModel(object):
61 class BaseModel(object):
62 """
62 """
63 Base Model for all classes
63 Base Model for all classes
64 """
64 """
65
65
66 @classmethod
66 @classmethod
67 def _get_keys(cls):
67 def _get_keys(cls):
68 """return column names for this model """
68 """return column names for this model """
69 return class_mapper(cls).c.keys()
69 return class_mapper(cls).c.keys()
70
70
71 def get_dict(self):
71 def get_dict(self):
72 """
72 """
73 return dict with keys and values corresponding
73 return dict with keys and values corresponding
74 to this model data """
74 to this model data """
75
75
76 d = {}
76 d = {}
77 for k in self._get_keys():
77 for k in self._get_keys():
78 d[k] = getattr(self, k)
78 d[k] = getattr(self, k)
79
79
80 # also use __json__() if present to get additional fields
80 # also use __json__() if present to get additional fields
81 _json_attr = getattr(self, '__json__', None)
81 _json_attr = getattr(self, '__json__', None)
82 if _json_attr:
82 if _json_attr:
83 # update with attributes from __json__
83 # update with attributes from __json__
84 if callable(_json_attr):
84 if callable(_json_attr):
85 _json_attr = _json_attr()
85 _json_attr = _json_attr()
86 for k, val in _json_attr.iteritems():
86 for k, val in _json_attr.iteritems():
87 d[k] = val
87 d[k] = val
88 return d
88 return d
89
89
90 def get_appstruct(self):
90 def get_appstruct(self):
91 """return list with keys and values tupples corresponding
91 """return list with keys and values tupples corresponding
92 to this model data """
92 to this model data """
93
93
94 l = []
94 l = []
95 for k in self._get_keys():
95 for k in self._get_keys():
96 l.append((k, getattr(self, k),))
96 l.append((k, getattr(self, k),))
97 return l
97 return l
98
98
99 def populate_obj(self, populate_dict):
99 def populate_obj(self, populate_dict):
100 """populate model with data from given populate_dict"""
100 """populate model with data from given populate_dict"""
101
101
102 for k in self._get_keys():
102 for k in self._get_keys():
103 if k in populate_dict:
103 if k in populate_dict:
104 setattr(self, k, populate_dict[k])
104 setattr(self, k, populate_dict[k])
105
105
106 @classmethod
106 @classmethod
107 def query(cls):
107 def query(cls):
108 return Session().query(cls)
108 return Session().query(cls)
109
109
110 @classmethod
110 @classmethod
111 def get(cls, id_):
111 def get(cls, id_):
112 if id_:
112 if id_:
113 return cls.query().get(id_)
113 return cls.query().get(id_)
114
114
115 @classmethod
115 @classmethod
116 def get_or_404(cls, id_):
116 def get_or_404(cls, id_):
117 try:
117 try:
118 id_ = int(id_)
118 id_ = int(id_)
119 except (TypeError, ValueError):
119 except (TypeError, ValueError):
120 raise HTTPNotFound
120 raise HTTPNotFound
121
121
122 res = cls.query().get(id_)
122 res = cls.query().get(id_)
123 if not res:
123 if not res:
124 raise HTTPNotFound
124 raise HTTPNotFound
125 return res
125 return res
126
126
127 @classmethod
127 @classmethod
128 def getAll(cls):
128 def getAll(cls):
129 # deprecated and left for backward compatibility
129 # deprecated and left for backward compatibility
130 return cls.get_all()
130 return cls.get_all()
131
131
132 @classmethod
132 @classmethod
133 def get_all(cls):
133 def get_all(cls):
134 return cls.query().all()
134 return cls.query().all()
135
135
136 @classmethod
136 @classmethod
137 def delete(cls, id_):
137 def delete(cls, id_):
138 obj = cls.query().get(id_)
138 obj = cls.query().get(id_)
139 Session().delete(obj)
139 Session().delete(obj)
140
140
141 def __repr__(self):
141 def __repr__(self):
142 if hasattr(self, '__unicode__'):
142 if hasattr(self, '__unicode__'):
143 # python repr needs to return str
143 # python repr needs to return str
144 return safe_str(self.__unicode__())
144 return safe_str(self.__unicode__())
145 return '<DB:%s>' % (self.__class__.__name__)
145 return '<DB:%s>' % (self.__class__.__name__)
146
146
147
147
148 class RhodeCodeSetting(Base, BaseModel):
148 class RhodeCodeSetting(Base, BaseModel):
149 __tablename__ = 'rhodecode_settings'
149 __tablename__ = 'rhodecode_settings'
150 __table_args__ = (
150 __table_args__ = (
151 UniqueConstraint('app_settings_name'),
151 UniqueConstraint('app_settings_name'),
152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
153 'mysql_charset': 'utf8'}
153 'mysql_charset': 'utf8'}
154 )
154 )
155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
156 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
157 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
157 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
158
158
159 def __init__(self, k='', v=''):
159 def __init__(self, k='', v=''):
160 self.app_settings_name = k
160 self.app_settings_name = k
161 self.app_settings_value = v
161 self.app_settings_value = v
162
162
163 @validates('_app_settings_value')
163 @validates('_app_settings_value')
164 def validate_settings_value(self, key, val):
164 def validate_settings_value(self, key, val):
165 assert type(val) == unicode
165 assert type(val) == unicode
166 return val
166 return val
167
167
168 @hybrid_property
168 @hybrid_property
169 def app_settings_value(self):
169 def app_settings_value(self):
170 v = self._app_settings_value
170 v = self._app_settings_value
171 if self.app_settings_name in ["ldap_active",
171 if self.app_settings_name in ["ldap_active",
172 "default_repo_enable_statistics",
172 "default_repo_enable_statistics",
173 "default_repo_enable_locking",
173 "default_repo_enable_locking",
174 "default_repo_private",
174 "default_repo_private",
175 "default_repo_enable_downloads"]:
175 "default_repo_enable_downloads"]:
176 v = str2bool(v)
176 v = str2bool(v)
177 return v
177 return v
178
178
179 @app_settings_value.setter
179 @app_settings_value.setter
180 def app_settings_value(self, val):
180 def app_settings_value(self, val):
181 """
181 """
182 Setter that will always make sure we use unicode in app_settings_value
182 Setter that will always make sure we use unicode in app_settings_value
183
183
184 :param val:
184 :param val:
185 """
185 """
186 self._app_settings_value = safe_unicode(val)
186 self._app_settings_value = safe_unicode(val)
187
187
188 def __unicode__(self):
188 def __unicode__(self):
189 return u"<%s('%s:%s')>" % (
189 return u"<%s('%s:%s')>" % (
190 self.__class__.__name__,
190 self.__class__.__name__,
191 self.app_settings_name, self.app_settings_value
191 self.app_settings_name, self.app_settings_value
192 )
192 )
193
193
194
194
195 class RhodeCodeUi(Base, BaseModel):
195 class RhodeCodeUi(Base, BaseModel):
196 __tablename__ = 'rhodecode_ui'
196 __tablename__ = 'rhodecode_ui'
197 __table_args__ = (
197 __table_args__ = (
198 UniqueConstraint('ui_key'),
198 UniqueConstraint('ui_key'),
199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
200 'mysql_charset': 'utf8'}
200 'mysql_charset': 'utf8'}
201 )
201 )
202
202
203 HOOK_REPO_SIZE = 'changegroup.repo_size'
203 HOOK_REPO_SIZE = 'changegroup.repo_size'
204 HOOK_PUSH = 'changegroup.push_logger'
204 HOOK_PUSH = 'changegroup.push_logger'
205 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
205 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
206 HOOK_PULL = 'outgoing.pull_logger'
206 HOOK_PULL = 'outgoing.pull_logger'
207 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
207 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
208
208
209 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
209 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
210 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
210 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
211 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
211 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
212 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
212 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
213 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
213 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
214
214
215
215
216
216
217 class User(Base, BaseModel):
217 class User(Base, BaseModel):
218 __tablename__ = 'users'
218 __tablename__ = 'users'
219 __table_args__ = (
219 __table_args__ = (
220 UniqueConstraint('username'), UniqueConstraint('email'),
220 UniqueConstraint('username'), UniqueConstraint('email'),
221 Index('u_username_idx', 'username'),
221 Index('u_username_idx', 'username'),
222 Index('u_email_idx', 'email'),
222 Index('u_email_idx', 'email'),
223 {'extend_existing': True, 'mysql_engine': 'InnoDB',
223 {'extend_existing': True, 'mysql_engine': 'InnoDB',
224 'mysql_charset': 'utf8'}
224 'mysql_charset': 'utf8'}
225 )
225 )
226 DEFAULT_USER = 'default'
226 DEFAULT_USER = 'default'
227 DEFAULT_PERMISSIONS = [
227 DEFAULT_PERMISSIONS = [
228 'hg.register.manual_activate', 'hg.create.repository',
228 'hg.register.manual_activate', 'hg.create.repository',
229 'hg.fork.repository', 'repository.read', 'group.read'
229 'hg.fork.repository', 'repository.read', 'group.read'
230 ]
230 ]
231 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
231 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
232 username = Column("username", String(255), nullable=True, unique=None, default=None)
232 username = Column("username", String(255), nullable=True, unique=None, default=None)
233 password = Column("password", String(255), nullable=True, unique=None, default=None)
233 password = Column("password", String(255), nullable=True, unique=None, default=None)
234 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
234 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
235 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
235 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
236 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
236 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
237 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
237 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
238 _email = Column("email", String(255), nullable=True, unique=None, default=None)
238 _email = Column("email", String(255), nullable=True, unique=None, default=None)
239 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
239 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
240 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
240 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
241 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
241 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
242 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
242 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
243
243
244 user_log = relationship('UserLog')
244 user_log = relationship('UserLog')
245 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
245 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
246
246
247 repositories = relationship('Repository')
247 repositories = relationship('Repository')
248 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
248 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
249 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
249 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
250
250
251 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
251 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
252 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
252 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
253
253
254 group_member = relationship('UserGroupMember', cascade='all')
254 group_member = relationship('UserGroupMember', cascade='all')
255
255
256 notifications = relationship('UserNotification', cascade='all')
256 notifications = relationship('UserNotification', cascade='all')
257 # notifications assigned to this user
257 # notifications assigned to this user
258 user_created_notifications = relationship('Notification', cascade='all')
258 user_created_notifications = relationship('Notification', cascade='all')
259 # comments created by this user
259 # comments created by this user
260 user_comments = relationship('ChangesetComment', cascade='all')
260 user_comments = relationship('ChangesetComment', cascade='all')
261 user_emails = relationship('UserEmailMap', cascade='all')
261 user_emails = relationship('UserEmailMap', cascade='all')
262
262
263 @hybrid_property
263 @hybrid_property
264 def email(self):
264 def email(self):
265 return self._email
265 return self._email
266
266
267 @email.setter
267 @email.setter
268 def email(self, val):
268 def email(self, val):
269 self._email = val.lower() if val else None
269 self._email = val.lower() if val else None
270
270
271 @property
271 @property
272 def firstname(self):
272 def firstname(self):
273 # alias for future
273 # alias for future
274 return self.name
274 return self.name
275
275
276 @property
276 @property
277 def username_and_name(self):
277 def username_and_name(self):
278 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
278 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
279
279
280 @property
280 @property
281 def full_name(self):
281 def full_name(self):
282 return '%s %s' % (self.firstname, self.lastname)
282 return '%s %s' % (self.firstname, self.lastname)
283
283
284 @property
284 @property
285 def full_contact(self):
285 def full_contact(self):
286 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
286 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
287
287
288 @property
288 @property
289 def short_contact(self):
289 def short_contact(self):
290 return '%s %s' % (self.firstname, self.lastname)
290 return '%s %s' % (self.firstname, self.lastname)
291
291
292 @property
292 @property
293 def is_admin(self):
293 def is_admin(self):
294 return self.admin
294 return self.admin
295
295
296 @classmethod
296 @classmethod
297 def get_by_username(cls, username, case_insensitive=False, cache=False):
297 def get_by_username(cls, username, case_insensitive=False, cache=False):
298 if case_insensitive:
298 if case_insensitive:
299 q = cls.query().filter(cls.username.ilike(username))
299 q = cls.query().filter(cls.username.ilike(username))
300 else:
300 else:
301 q = cls.query().filter(cls.username == username)
301 q = cls.query().filter(cls.username == username)
302
302
303 if cache:
303 if cache:
304 q = q.options(FromCache(
304 q = q.options(FromCache(
305 "sql_cache_short",
305 "sql_cache_short",
306 "get_user_%s" % _hash_key(username)
306 "get_user_%s" % _hash_key(username)
307 )
307 )
308 )
308 )
309 return q.scalar()
309 return q.scalar()
310
310
311 @classmethod
311 @classmethod
312 def get_by_auth_token(cls, auth_token, cache=False):
312 def get_by_auth_token(cls, auth_token, cache=False):
313 q = cls.query().filter(cls.api_key == auth_token)
313 q = cls.query().filter(cls.api_key == auth_token)
314
314
315 if cache:
315 if cache:
316 q = q.options(FromCache("sql_cache_short",
316 q = q.options(FromCache("sql_cache_short",
317 "get_auth_token_%s" % auth_token))
317 "get_auth_token_%s" % auth_token))
318 return q.scalar()
318 return q.scalar()
319
319
320 @classmethod
320 @classmethod
321 def get_by_email(cls, email, case_insensitive=False, cache=False):
321 def get_by_email(cls, email, case_insensitive=False, cache=False):
322 if case_insensitive:
322 if case_insensitive:
323 q = cls.query().filter(cls.email.ilike(email))
323 q = cls.query().filter(cls.email.ilike(email))
324 else:
324 else:
325 q = cls.query().filter(cls.email == email)
325 q = cls.query().filter(cls.email == email)
326
326
327 if cache:
327 if cache:
328 q = q.options(FromCache("sql_cache_short",
328 q = q.options(FromCache("sql_cache_short",
329 "get_email_key_%s" % email))
329 "get_email_key_%s" % email))
330
330
331 ret = q.scalar()
331 ret = q.scalar()
332 if ret is None:
332 if ret is None:
333 q = UserEmailMap.query()
333 q = UserEmailMap.query()
334 # try fetching in alternate email map
334 # try fetching in alternate email map
335 if case_insensitive:
335 if case_insensitive:
336 q = q.filter(UserEmailMap.email.ilike(email))
336 q = q.filter(UserEmailMap.email.ilike(email))
337 else:
337 else:
338 q = q.filter(UserEmailMap.email == email)
338 q = q.filter(UserEmailMap.email == email)
339 q = q.options(joinedload(UserEmailMap.user))
339 q = q.options(joinedload(UserEmailMap.user))
340 if cache:
340 if cache:
341 q = q.options(FromCache("sql_cache_short",
341 q = q.options(FromCache("sql_cache_short",
342 "get_email_map_key_%s" % email))
342 "get_email_map_key_%s" % email))
343 ret = getattr(q.scalar(), 'user', None)
343 ret = getattr(q.scalar(), 'user', None)
344
344
345 return ret
345 return ret
346
346
347
347
348 class UserEmailMap(Base, BaseModel):
348 class UserEmailMap(Base, BaseModel):
349 __tablename__ = 'user_email_map'
349 __tablename__ = 'user_email_map'
350 __table_args__ = (
350 __table_args__ = (
351 Index('uem_email_idx', 'email'),
351 Index('uem_email_idx', 'email'),
352 UniqueConstraint('email'),
352 UniqueConstraint('email'),
353 {'extend_existing': True, 'mysql_engine': 'InnoDB',
353 {'extend_existing': True, 'mysql_engine': 'InnoDB',
354 'mysql_charset': 'utf8'}
354 'mysql_charset': 'utf8'}
355 )
355 )
356 __mapper_args__ = {}
356 __mapper_args__ = {}
357
357
358 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
358 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
359 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
359 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
360 _email = Column("email", String(255), nullable=True, unique=False, default=None)
360 _email = Column("email", String(255), nullable=True, unique=False, default=None)
361 user = relationship('User', lazy='joined')
361 user = relationship('User', lazy='joined')
362
362
363 @validates('_email')
363 @validates('_email')
364 def validate_email(self, key, email):
364 def validate_email(self, key, email):
365 # check if this email is not main one
365 # check if this email is not main one
366 main_email = Session().query(User).filter(User.email == email).scalar()
366 main_email = Session().query(User).filter(User.email == email).scalar()
367 if main_email is not None:
367 if main_email is not None:
368 raise AttributeError('email %s is present is user table' % email)
368 raise AttributeError('email %s is present is user table' % email)
369 return email
369 return email
370
370
371 @hybrid_property
371 @hybrid_property
372 def email(self):
372 def email(self):
373 return self._email
373 return self._email
374
374
375 @email.setter
375 @email.setter
376 def email(self, val):
376 def email(self, val):
377 self._email = val.lower() if val else None
377 self._email = val.lower() if val else None
378
378
379
379
380 class UserIpMap(Base, BaseModel):
380 class UserIpMap(Base, BaseModel):
381 __tablename__ = 'user_ip_map'
381 __tablename__ = 'user_ip_map'
382 __table_args__ = (
382 __table_args__ = (
383 UniqueConstraint('user_id', 'ip_addr'),
383 UniqueConstraint('user_id', 'ip_addr'),
384 {'extend_existing': True, 'mysql_engine': 'InnoDB',
384 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 'mysql_charset': 'utf8'}
385 'mysql_charset': 'utf8'}
386 )
386 )
387 __mapper_args__ = {}
387 __mapper_args__ = {}
388
388
389 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
389 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
391 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
391 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
392 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
392 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
393 user = relationship('User', lazy='joined')
393 user = relationship('User', lazy='joined')
394
394
395
395
396 class UserLog(Base, BaseModel):
396 class UserLog(Base, BaseModel):
397 __tablename__ = 'user_logs'
397 __tablename__ = 'user_logs'
398 __table_args__ = (
398 __table_args__ = (
399 {'extend_existing': True, 'mysql_engine': 'InnoDB',
399 {'extend_existing': True, 'mysql_engine': 'InnoDB',
400 'mysql_charset': 'utf8'},
400 'mysql_charset': 'utf8'},
401 )
401 )
402 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
402 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
403 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
403 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
404 username = Column("username", String(255), nullable=True, unique=None, default=None)
404 username = Column("username", String(255), nullable=True, unique=None, default=None)
405 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
405 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
406 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
406 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
407 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
407 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
408 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
408 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
409 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
409 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
410
410
411
411
412 user = relationship('User')
412 user = relationship('User')
413 repository = relationship('Repository', cascade='')
413 repository = relationship('Repository', cascade='')
414
414
415
415
416 class UserGroup(Base, BaseModel):
416 class UserGroup(Base, BaseModel):
417 __tablename__ = 'users_groups'
417 __tablename__ = 'users_groups'
418 __table_args__ = (
418 __table_args__ = (
419 {'extend_existing': True, 'mysql_engine': 'InnoDB',
419 {'extend_existing': True, 'mysql_engine': 'InnoDB',
420 'mysql_charset': 'utf8'},
420 'mysql_charset': 'utf8'},
421 )
421 )
422
422
423 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
423 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
424 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
424 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
425 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
425 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
426 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
426 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
427
427
428 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
428 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
429 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
429 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
430 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
430 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
431
431
432 def __unicode__(self):
432 def __unicode__(self):
433 return u'<userGroup(%s)>' % (self.users_group_name)
433 return u'<userGroup(%s)>' % (self.users_group_name)
434
434
435 @classmethod
435 @classmethod
436 def get_by_group_name(cls, group_name, cache=False,
436 def get_by_group_name(cls, group_name, cache=False,
437 case_insensitive=False):
437 case_insensitive=False):
438 if case_insensitive:
438 if case_insensitive:
439 q = cls.query().filter(cls.users_group_name.ilike(group_name))
439 q = cls.query().filter(cls.users_group_name.ilike(group_name))
440 else:
440 else:
441 q = cls.query().filter(cls.users_group_name == group_name)
441 q = cls.query().filter(cls.users_group_name == group_name)
442 if cache:
442 if cache:
443 q = q.options(FromCache(
443 q = q.options(FromCache(
444 "sql_cache_short",
444 "sql_cache_short",
445 "get_user_%s" % _hash_key(group_name)
445 "get_user_%s" % _hash_key(group_name)
446 )
446 )
447 )
447 )
448 return q.scalar()
448 return q.scalar()
449
449
450 @classmethod
450 @classmethod
451 def get(cls, users_group_id, cache=False):
451 def get(cls, users_group_id, cache=False):
452 user_group = cls.query()
452 user_group = cls.query()
453 if cache:
453 if cache:
454 user_group = user_group.options(FromCache("sql_cache_short",
454 user_group = user_group.options(FromCache("sql_cache_short",
455 "get_users_group_%s" % users_group_id))
455 "get_users_group_%s" % users_group_id))
456 return user_group.get(users_group_id)
456 return user_group.get(users_group_id)
457
457
458
458
459 class UserGroupMember(Base, BaseModel):
459 class UserGroupMember(Base, BaseModel):
460 __tablename__ = 'users_groups_members'
460 __tablename__ = 'users_groups_members'
461 __table_args__ = (
461 __table_args__ = (
462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
463 'mysql_charset': 'utf8'},
463 'mysql_charset': 'utf8'},
464 )
464 )
465
465
466 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
466 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
467 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
467 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
468 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
468 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
469
469
470 user = relationship('User', lazy='joined')
470 user = relationship('User', lazy='joined')
471 users_group = relationship('UserGroup')
471 users_group = relationship('UserGroup')
472
472
473 def __init__(self, gr_id='', u_id=''):
473 def __init__(self, gr_id='', u_id=''):
474 self.users_group_id = gr_id
474 self.users_group_id = gr_id
475 self.user_id = u_id
475 self.user_id = u_id
476
476
477
477
478 class RepositoryField(Base, BaseModel):
478 class RepositoryField(Base, BaseModel):
479 __tablename__ = 'repositories_fields'
479 __tablename__ = 'repositories_fields'
480 __table_args__ = (
480 __table_args__ = (
481 UniqueConstraint('repository_id', 'field_key'), # no-multi field
481 UniqueConstraint('repository_id', 'field_key'), # no-multi field
482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
483 'mysql_charset': 'utf8'},
483 'mysql_charset': 'utf8'},
484 )
484 )
485 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
485 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
486
486
487 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
487 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
489 field_key = Column("field_key", String(250))
489 field_key = Column("field_key", String(250))
490 field_label = Column("field_label", String(1024), nullable=False)
490 field_label = Column("field_label", String(1024), nullable=False)
491 field_value = Column("field_value", String(10000), nullable=False)
491 field_value = Column("field_value", String(10000), nullable=False)
492 field_desc = Column("field_desc", String(1024), nullable=False)
492 field_desc = Column("field_desc", String(1024), nullable=False)
493 field_type = Column("field_type", String(256), nullable=False, unique=None)
493 field_type = Column("field_type", String(256), nullable=False, unique=None)
494 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
494 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
495
495
496 repository = relationship('Repository')
496 repository = relationship('Repository')
497
497
498 @classmethod
498 @classmethod
499 def get_by_key_name(cls, key, repo):
499 def get_by_key_name(cls, key, repo):
500 row = cls.query()\
500 row = cls.query()\
501 .filter(cls.repository == repo)\
501 .filter(cls.repository == repo)\
502 .filter(cls.field_key == key).scalar()
502 .filter(cls.field_key == key).scalar()
503 return row
503 return row
504
504
505
505
506 class Repository(Base, BaseModel):
506 class Repository(Base, BaseModel):
507 __tablename__ = 'repositories'
507 __tablename__ = 'repositories'
508 __table_args__ = (
508 __table_args__ = (
509 UniqueConstraint('repo_name'),
509 UniqueConstraint('repo_name'),
510 Index('r_repo_name_idx', 'repo_name'),
510 Index('r_repo_name_idx', 'repo_name'),
511 {'extend_existing': True, 'mysql_engine': 'InnoDB',
511 {'extend_existing': True, 'mysql_engine': 'InnoDB',
512 'mysql_charset': 'utf8'},
512 'mysql_charset': 'utf8'},
513 )
513 )
514
514
515 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
515 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
516 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
516 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
517 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
517 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
518 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default=None)
518 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default=None)
519 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
519 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
520 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
520 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
521 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
521 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
522 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
522 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
523 description = Column("description", String(10000), nullable=True, unique=None, default=None)
523 description = Column("description", String(10000), nullable=True, unique=None, default=None)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
525 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
525 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
526 landing_rev = Column("landing_revision", String(255), nullable=False, unique=False, default=None)
526 landing_rev = Column("landing_revision", String(255), nullable=False, unique=False, default=None)
527 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
527 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
528 _locked = Column("locked", String(255), nullable=True, unique=False, default=None)
528 _locked = Column("locked", String(255), nullable=True, unique=False, default=None)
529 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
529 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
530
530
531 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
531 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
532 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
532 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
533
533
534 user = relationship('User')
534 user = relationship('User')
535 fork = relationship('Repository', remote_side=repo_id)
535 fork = relationship('Repository', remote_side=repo_id)
536 group = relationship('RepoGroup')
536 group = relationship('RepoGroup')
537 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
537 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
538 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
538 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
539 stats = relationship('Statistics', cascade='all', uselist=False)
539 stats = relationship('Statistics', cascade='all', uselist=False)
540
540
541 followers = relationship('UserFollowing',
541 followers = relationship('UserFollowing',
542 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
542 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
543 cascade='all')
543 cascade='all')
544 extra_fields = relationship('RepositoryField',
544 extra_fields = relationship('RepositoryField',
545 cascade="all, delete, delete-orphan")
545 cascade="all, delete, delete-orphan")
546
546
547 logs = relationship('UserLog')
547 logs = relationship('UserLog')
548 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
548 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
549
549
550 pull_requests_org = relationship('PullRequest',
550 pull_requests_org = relationship('PullRequest',
551 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
551 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
552 cascade="all, delete, delete-orphan")
552 cascade="all, delete, delete-orphan")
553
553
554 pull_requests_other = relationship('PullRequest',
554 pull_requests_other = relationship('PullRequest',
555 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
555 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
556 cascade="all, delete, delete-orphan")
556 cascade="all, delete, delete-orphan")
557
557
558 def __unicode__(self):
558 def __unicode__(self):
559 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
559 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
560 safe_unicode(self.repo_name))
560 safe_unicode(self.repo_name))
561
561
562 #NOTE for this migration we are required tio have it
562 #NOTE for this migration we are required tio have it
563 @hybrid_property
563 @hybrid_property
564 def changeset_cache(self):
564 def changeset_cache(self):
565 from rhodecode.lib.vcs.backends.base import EmptyCommit
565 from rhodecode.lib.vcs.backends.base import EmptyCommit
566 dummy = EmptyCommit().__json__()
566 dummy = EmptyCommit().__json__()
567 if not self._changeset_cache:
567 if not self._changeset_cache:
568 return dummy
568 return dummy
569 try:
569 try:
570 return json.loads(self._changeset_cache)
570 return json.loads(self._changeset_cache)
571 except TypeError:
571 except TypeError:
572 return dummy
572 return dummy
573
573
574 @changeset_cache.setter
574 @changeset_cache.setter
575 def changeset_cache(self, val):
575 def changeset_cache(self, val):
576 try:
576 try:
577 self._changeset_cache = json.dumps(val)
577 self._changeset_cache = json.dumps(val)
578 except Exception:
578 except Exception:
579 log.error(traceback.format_exc())
579 log.error(traceback.format_exc())
580
580
581 @classmethod
581 @classmethod
582 def get_by_repo_name(cls, repo_name):
582 def get_by_repo_name(cls, repo_name):
583 q = Session().query(cls).filter(cls.repo_name == repo_name)
583 q = Session().query(cls).filter(cls.repo_name == repo_name)
584 q = q.options(joinedload(Repository.fork))\
584 q = q.options(joinedload(Repository.fork))\
585 .options(joinedload(Repository.user))\
585 .options(joinedload(Repository.user))\
586 .options(joinedload(Repository.group))
586 .options(joinedload(Repository.group))
587 return q.scalar()
587 return q.scalar()
588
588
589 #NOTE this is required for this migration to work
589 #NOTE this is required for this migration to work
590 def update_commit_cache(self, cs_cache=None):
590 def update_commit_cache(self, cs_cache=None):
591 """
591 """
592 Update cache of last changeset for repository, keys should be::
592 Update cache of last changeset for repository, keys should be::
593
593
594 short_id
594 short_id
595 raw_id
595 raw_id
596 revision
596 revision
597 message
597 message
598 date
598 date
599 author
599 author
600
600
601 :param cs_cache:
601 :param cs_cache:
602 """
602 """
603 from rhodecode.lib.vcs.backends.base import BaseChangeset
603 from rhodecode.lib.vcs.backends.base import BaseChangeset
604 if cs_cache is None:
604 if cs_cache is None:
605 cs_cache = EmptyCommit()
605 cs_cache = EmptyCommit()
606 # Note: Using always the empty commit here in case we are
606 # Note: Using always the empty commit here in case we are
607 # upgrading towards version 3.0 and above. Reason is that in this
607 # upgrading towards version 3.0 and above. Reason is that in this
608 # case the vcsclient connection is not available and things
608 # case the vcsclient connection is not available and things
609 # would explode here.
609 # would explode here.
610
610
611 if isinstance(cs_cache, BaseChangeset):
611 if isinstance(cs_cache, BaseChangeset):
612 cs_cache = cs_cache.__json__()
612 cs_cache = cs_cache.__json__()
613
613
614 if (cs_cache != self.changeset_cache or not self.changeset_cache):
614 if (cs_cache != self.changeset_cache or not self.changeset_cache):
615 _default = datetime.datetime.fromtimestamp(0)
615 _default = datetime.datetime.fromtimestamp(0)
616 last_change = cs_cache.get('date') or _default
616 last_change = cs_cache.get('date') or _default
617 log.debug('updated repo %s with new cs cache %s'
617 log.debug('updated repo %s with new cs cache %s', self.repo_name, cs_cache)
618 % (self.repo_name, cs_cache))
619 self.updated_on = last_change
618 self.updated_on = last_change
620 self.changeset_cache = cs_cache
619 self.changeset_cache = cs_cache
621 Session().add(self)
620 Session().add(self)
622 Session().commit()
621 Session().commit()
623 else:
622 else:
624 log.debug('Skipping repo:%s already with latest changes'
623 log.debug('Skipping repo:%s already with latest changes', self.repo_name)
625 % self.repo_name)
626
624
627 class RepoGroup(Base, BaseModel):
625 class RepoGroup(Base, BaseModel):
628 __tablename__ = 'groups'
626 __tablename__ = 'groups'
629 __table_args__ = (
627 __table_args__ = (
630 UniqueConstraint('group_name', 'group_parent_id'),
628 UniqueConstraint('group_name', 'group_parent_id'),
631 CheckConstraint('group_id != group_parent_id'),
629 CheckConstraint('group_id != group_parent_id'),
632 {'extend_existing': True, 'mysql_engine': 'InnoDB',
630 {'extend_existing': True, 'mysql_engine': 'InnoDB',
633 'mysql_charset': 'utf8'},
631 'mysql_charset': 'utf8'},
634 )
632 )
635 __mapper_args__ = {'order_by': 'group_name'}
633 __mapper_args__ = {'order_by': 'group_name'}
636
634
637 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
635 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
638 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
636 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
639 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
637 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
640 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
638 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
641 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
639 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
642
640
643 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
641 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
644 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
642 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
645 parent_group = relationship('RepoGroup', remote_side=group_id)
643 parent_group = relationship('RepoGroup', remote_side=group_id)
646
644
647 def __init__(self, group_name='', parent_group=None):
645 def __init__(self, group_name='', parent_group=None):
648 self.group_name = group_name
646 self.group_name = group_name
649 self.parent_group = parent_group
647 self.parent_group = parent_group
650
648
651 def __unicode__(self):
649 def __unicode__(self):
652 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
650 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
653 self.group_name)
651 self.group_name)
654
652
655 @classmethod
653 @classmethod
656 def url_sep(cls):
654 def url_sep(cls):
657 return URL_SEP
655 return URL_SEP
658
656
659 @classmethod
657 @classmethod
660 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
658 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
661 if case_insensitive:
659 if case_insensitive:
662 gr = cls.query()\
660 gr = cls.query()\
663 .filter(cls.group_name.ilike(group_name))
661 .filter(cls.group_name.ilike(group_name))
664 else:
662 else:
665 gr = cls.query()\
663 gr = cls.query()\
666 .filter(cls.group_name == group_name)
664 .filter(cls.group_name == group_name)
667 if cache:
665 if cache:
668 gr = gr.options(FromCache(
666 gr = gr.options(FromCache(
669 "sql_cache_short",
667 "sql_cache_short",
670 "get_group_%s" % _hash_key(group_name)
668 "get_group_%s" % _hash_key(group_name)
671 )
669 )
672 )
670 )
673 return gr.scalar()
671 return gr.scalar()
674
672
675
673
676 class Permission(Base, BaseModel):
674 class Permission(Base, BaseModel):
677 __tablename__ = 'permissions'
675 __tablename__ = 'permissions'
678 __table_args__ = (
676 __table_args__ = (
679 Index('p_perm_name_idx', 'permission_name'),
677 Index('p_perm_name_idx', 'permission_name'),
680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
681 'mysql_charset': 'utf8'},
679 'mysql_charset': 'utf8'},
682 )
680 )
683 PERMS = [
681 PERMS = [
684 ('repository.none', _('Repository no access')),
682 ('repository.none', _('Repository no access')),
685 ('repository.read', _('Repository read access')),
683 ('repository.read', _('Repository read access')),
686 ('repository.write', _('Repository write access')),
684 ('repository.write', _('Repository write access')),
687 ('repository.admin', _('Repository admin access')),
685 ('repository.admin', _('Repository admin access')),
688
686
689 ('group.none', _('Repository group no access')),
687 ('group.none', _('Repository group no access')),
690 ('group.read', _('Repository group read access')),
688 ('group.read', _('Repository group read access')),
691 ('group.write', _('Repository group write access')),
689 ('group.write', _('Repository group write access')),
692 ('group.admin', _('Repository group admin access')),
690 ('group.admin', _('Repository group admin access')),
693
691
694 ('hg.admin', _('RhodeCode Administrator')),
692 ('hg.admin', _('RhodeCode Administrator')),
695 ('hg.create.none', _('Repository creation disabled')),
693 ('hg.create.none', _('Repository creation disabled')),
696 ('hg.create.repository', _('Repository creation enabled')),
694 ('hg.create.repository', _('Repository creation enabled')),
697 ('hg.fork.none', _('Repository forking disabled')),
695 ('hg.fork.none', _('Repository forking disabled')),
698 ('hg.fork.repository', _('Repository forking enabled')),
696 ('hg.fork.repository', _('Repository forking enabled')),
699 ('hg.register.none', _('Register disabled')),
697 ('hg.register.none', _('Register disabled')),
700 ('hg.register.manual_activate', _('Register new user with RhodeCode '
698 ('hg.register.manual_activate', _('Register new user with RhodeCode '
701 'with manual activation')),
699 'with manual activation')),
702
700
703 ('hg.register.auto_activate', _('Register new user with RhodeCode '
701 ('hg.register.auto_activate', _('Register new user with RhodeCode '
704 'with auto activation')),
702 'with auto activation')),
705 ]
703 ]
706
704
707 # defines which permissions are more important higher the more important
705 # defines which permissions are more important higher the more important
708 PERM_WEIGHTS = {
706 PERM_WEIGHTS = {
709 'repository.none': 0,
707 'repository.none': 0,
710 'repository.read': 1,
708 'repository.read': 1,
711 'repository.write': 3,
709 'repository.write': 3,
712 'repository.admin': 4,
710 'repository.admin': 4,
713
711
714 'group.none': 0,
712 'group.none': 0,
715 'group.read': 1,
713 'group.read': 1,
716 'group.write': 3,
714 'group.write': 3,
717 'group.admin': 4,
715 'group.admin': 4,
718
716
719 'hg.fork.none': 0,
717 'hg.fork.none': 0,
720 'hg.fork.repository': 1,
718 'hg.fork.repository': 1,
721 'hg.create.none': 0,
719 'hg.create.none': 0,
722 'hg.create.repository':1
720 'hg.create.repository':1
723 }
721 }
724
722
725 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
723 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
726 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
724 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
727 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
725 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
728
726
729 def __unicode__(self):
727 def __unicode__(self):
730 return u"<%s('%s:%s')>" % (
728 return u"<%s('%s:%s')>" % (
731 self.__class__.__name__, self.permission_id, self.permission_name
729 self.__class__.__name__, self.permission_id, self.permission_name
732 )
730 )
733
731
734 @classmethod
732 @classmethod
735 def get_by_key(cls, key):
733 def get_by_key(cls, key):
736 return cls.query().filter(cls.permission_name == key).scalar()
734 return cls.query().filter(cls.permission_name == key).scalar()
737
735
738
736
739 class UserRepoToPerm(Base, BaseModel):
737 class UserRepoToPerm(Base, BaseModel):
740 __tablename__ = 'repo_to_perm'
738 __tablename__ = 'repo_to_perm'
741 __table_args__ = (
739 __table_args__ = (
742 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
740 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
743 {'extend_existing': True, 'mysql_engine': 'InnoDB',
741 {'extend_existing': True, 'mysql_engine': 'InnoDB',
744 'mysql_charset': 'utf8'}
742 'mysql_charset': 'utf8'}
745 )
743 )
746 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
744 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
745 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
748 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
746 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
749 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
747 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
750
748
751 user = relationship('User')
749 user = relationship('User')
752 repository = relationship('Repository')
750 repository = relationship('Repository')
753 permission = relationship('Permission')
751 permission = relationship('Permission')
754
752
755 def __unicode__(self):
753 def __unicode__(self):
756 return u'<user:%s => %s >' % (self.user, self.repository)
754 return u'<user:%s => %s >' % (self.user, self.repository)
757
755
758
756
759 class UserToPerm(Base, BaseModel):
757 class UserToPerm(Base, BaseModel):
760 __tablename__ = 'user_to_perm'
758 __tablename__ = 'user_to_perm'
761 __table_args__ = (
759 __table_args__ = (
762 UniqueConstraint('user_id', 'permission_id'),
760 UniqueConstraint('user_id', 'permission_id'),
763 {'extend_existing': True, 'mysql_engine': 'InnoDB',
761 {'extend_existing': True, 'mysql_engine': 'InnoDB',
764 'mysql_charset': 'utf8'}
762 'mysql_charset': 'utf8'}
765 )
763 )
766 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
764 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
767 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
765 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
768 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
766 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
769
767
770 user = relationship('User')
768 user = relationship('User')
771 permission = relationship('Permission', lazy='joined')
769 permission = relationship('Permission', lazy='joined')
772
770
773
771
774 class UserGroupRepoToPerm(Base, BaseModel):
772 class UserGroupRepoToPerm(Base, BaseModel):
775 __tablename__ = 'users_group_repo_to_perm'
773 __tablename__ = 'users_group_repo_to_perm'
776 __table_args__ = (
774 __table_args__ = (
777 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
775 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
778 {'extend_existing': True, 'mysql_engine': 'InnoDB',
776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
779 'mysql_charset': 'utf8'}
777 'mysql_charset': 'utf8'}
780 )
778 )
781 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
779 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
782 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
780 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
783 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
781 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
784 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
782 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
785
783
786 users_group = relationship('UserGroup')
784 users_group = relationship('UserGroup')
787 permission = relationship('Permission')
785 permission = relationship('Permission')
788 repository = relationship('Repository')
786 repository = relationship('Repository')
789
787
790 def __unicode__(self):
788 def __unicode__(self):
791 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
789 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
792
790
793
791
794 class UserGroupToPerm(Base, BaseModel):
792 class UserGroupToPerm(Base, BaseModel):
795 __tablename__ = 'users_group_to_perm'
793 __tablename__ = 'users_group_to_perm'
796 __table_args__ = (
794 __table_args__ = (
797 UniqueConstraint('users_group_id', 'permission_id',),
795 UniqueConstraint('users_group_id', 'permission_id',),
798 {'extend_existing': True, 'mysql_engine': 'InnoDB',
796 {'extend_existing': True, 'mysql_engine': 'InnoDB',
799 'mysql_charset': 'utf8'}
797 'mysql_charset': 'utf8'}
800 )
798 )
801 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
799 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
802 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
800 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
803 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
801 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
804
802
805 users_group = relationship('UserGroup')
803 users_group = relationship('UserGroup')
806 permission = relationship('Permission')
804 permission = relationship('Permission')
807
805
808
806
809 class UserRepoGroupToPerm(Base, BaseModel):
807 class UserRepoGroupToPerm(Base, BaseModel):
810 __tablename__ = 'user_repo_group_to_perm'
808 __tablename__ = 'user_repo_group_to_perm'
811 __table_args__ = (
809 __table_args__ = (
812 UniqueConstraint('user_id', 'group_id', 'permission_id'),
810 UniqueConstraint('user_id', 'group_id', 'permission_id'),
813 {'extend_existing': True, 'mysql_engine': 'InnoDB',
811 {'extend_existing': True, 'mysql_engine': 'InnoDB',
814 'mysql_charset': 'utf8'}
812 'mysql_charset': 'utf8'}
815 )
813 )
816
814
817 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
815 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
818 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
816 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
819 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
817 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
820 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
821
819
822 user = relationship('User')
820 user = relationship('User')
823 group = relationship('RepoGroup')
821 group = relationship('RepoGroup')
824 permission = relationship('Permission')
822 permission = relationship('Permission')
825
823
826
824
827 class UserGroupRepoGroupToPerm(Base, BaseModel):
825 class UserGroupRepoGroupToPerm(Base, BaseModel):
828 __tablename__ = 'users_group_repo_group_to_perm'
826 __tablename__ = 'users_group_repo_group_to_perm'
829 __table_args__ = (
827 __table_args__ = (
830 UniqueConstraint('users_group_id', 'group_id'),
828 UniqueConstraint('users_group_id', 'group_id'),
831 {'extend_existing': True, 'mysql_engine': 'InnoDB',
829 {'extend_existing': True, 'mysql_engine': 'InnoDB',
832 'mysql_charset': 'utf8'}
830 'mysql_charset': 'utf8'}
833 )
831 )
834
832
835 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)
833 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)
836 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
834 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
837 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
835 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
838 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
836 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
839
837
840 users_group = relationship('UserGroup')
838 users_group = relationship('UserGroup')
841 permission = relationship('Permission')
839 permission = relationship('Permission')
842 group = relationship('RepoGroup')
840 group = relationship('RepoGroup')
843
841
844
842
845 class Statistics(Base, BaseModel):
843 class Statistics(Base, BaseModel):
846 __tablename__ = 'statistics'
844 __tablename__ = 'statistics'
847 __table_args__ = (
845 __table_args__ = (
848 UniqueConstraint('repository_id'),
846 UniqueConstraint('repository_id'),
849 {'extend_existing': True, 'mysql_engine': 'InnoDB',
847 {'extend_existing': True, 'mysql_engine': 'InnoDB',
850 'mysql_charset': 'utf8'}
848 'mysql_charset': 'utf8'}
851 )
849 )
852 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
850 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
853 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
851 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
854 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
852 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
855 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
853 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
856 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
854 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
857 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
855 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
858
856
859 repository = relationship('Repository', single_parent=True)
857 repository = relationship('Repository', single_parent=True)
860
858
861
859
862 class UserFollowing(Base, BaseModel):
860 class UserFollowing(Base, BaseModel):
863 __tablename__ = 'user_followings'
861 __tablename__ = 'user_followings'
864 __table_args__ = (
862 __table_args__ = (
865 UniqueConstraint('user_id', 'follows_repository_id'),
863 UniqueConstraint('user_id', 'follows_repository_id'),
866 UniqueConstraint('user_id', 'follows_user_id'),
864 UniqueConstraint('user_id', 'follows_user_id'),
867 {'extend_existing': True, 'mysql_engine': 'InnoDB',
865 {'extend_existing': True, 'mysql_engine': 'InnoDB',
868 'mysql_charset': 'utf8'}
866 'mysql_charset': 'utf8'}
869 )
867 )
870
868
871 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
869 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
872 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
870 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
873 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
871 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
874 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
872 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
875 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
873 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
876
874
877 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
875 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
878
876
879 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
877 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
880 follows_repository = relationship('Repository', order_by='Repository.repo_name')
878 follows_repository = relationship('Repository', order_by='Repository.repo_name')
881
879
882
880
883 class CacheInvalidation(Base, BaseModel):
881 class CacheInvalidation(Base, BaseModel):
884 __tablename__ = 'cache_invalidation'
882 __tablename__ = 'cache_invalidation'
885 __table_args__ = (
883 __table_args__ = (
886 UniqueConstraint('cache_key'),
884 UniqueConstraint('cache_key'),
887 Index('key_idx', 'cache_key'),
885 Index('key_idx', 'cache_key'),
888 {'extend_existing': True, 'mysql_engine': 'InnoDB',
886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
889 'mysql_charset': 'utf8'},
887 'mysql_charset': 'utf8'},
890 )
888 )
891 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
889 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
892 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
890 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
893 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
891 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
894 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
892 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
895
893
896 def __init__(self, cache_key, cache_args=''):
894 def __init__(self, cache_key, cache_args=''):
897 self.cache_key = cache_key
895 self.cache_key = cache_key
898 self.cache_args = cache_args
896 self.cache_args = cache_args
899 self.cache_active = False
897 self.cache_active = False
900
898
901
899
902 class ChangesetComment(Base, BaseModel):
900 class ChangesetComment(Base, BaseModel):
903 __tablename__ = 'changeset_comments'
901 __tablename__ = 'changeset_comments'
904 __table_args__ = (
902 __table_args__ = (
905 Index('cc_revision_idx', 'revision'),
903 Index('cc_revision_idx', 'revision'),
906 {'extend_existing': True, 'mysql_engine': 'InnoDB',
904 {'extend_existing': True, 'mysql_engine': 'InnoDB',
907 'mysql_charset': 'utf8'},
905 'mysql_charset': 'utf8'},
908 )
906 )
909 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
907 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
910 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
908 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
911 revision = Column('revision', String(40), nullable=True)
909 revision = Column('revision', String(40), nullable=True)
912 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
910 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
913 line_no = Column('line_no', Unicode(10), nullable=True)
911 line_no = Column('line_no', Unicode(10), nullable=True)
914 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
912 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
915 f_path = Column('f_path', Unicode(1000), nullable=True)
913 f_path = Column('f_path', Unicode(1000), nullable=True)
916 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
914 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
917 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
915 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
918 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
916 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
919 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
917 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
920
918
921 author = relationship('User', lazy='joined')
919 author = relationship('User', lazy='joined')
922 repo = relationship('Repository')
920 repo = relationship('Repository')
923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
921 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
924 pull_request = relationship('PullRequest', lazy='joined')
922 pull_request = relationship('PullRequest', lazy='joined')
925
923
926 @classmethod
924 @classmethod
927 def get_users(cls, revision=None, pull_request_id=None):
925 def get_users(cls, revision=None, pull_request_id=None):
928 """
926 """
929 Returns user associated with this ChangesetComment. ie those
927 Returns user associated with this ChangesetComment. ie those
930 who actually commented
928 who actually commented
931
929
932 :param cls:
930 :param cls:
933 :param revision:
931 :param revision:
934 """
932 """
935 q = Session().query(User)\
933 q = Session().query(User)\
936 .join(ChangesetComment.author)
934 .join(ChangesetComment.author)
937 if revision:
935 if revision:
938 q = q.filter(cls.revision == revision)
936 q = q.filter(cls.revision == revision)
939 elif pull_request_id:
937 elif pull_request_id:
940 q = q.filter(cls.pull_request_id == pull_request_id)
938 q = q.filter(cls.pull_request_id == pull_request_id)
941 return q.all()
939 return q.all()
942
940
943
941
944 class ChangesetStatus(Base, BaseModel):
942 class ChangesetStatus(Base, BaseModel):
945 __tablename__ = 'changeset_statuses'
943 __tablename__ = 'changeset_statuses'
946 __table_args__ = (
944 __table_args__ = (
947 Index('cs_revision_idx', 'revision'),
945 Index('cs_revision_idx', 'revision'),
948 Index('cs_version_idx', 'version'),
946 Index('cs_version_idx', 'version'),
949 UniqueConstraint('repo_id', 'revision', 'version'),
947 UniqueConstraint('repo_id', 'revision', 'version'),
950 {'extend_existing': True, 'mysql_engine': 'InnoDB',
948 {'extend_existing': True, 'mysql_engine': 'InnoDB',
951 'mysql_charset': 'utf8'}
949 'mysql_charset': 'utf8'}
952 )
950 )
953 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
951 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
954 STATUS_APPROVED = 'approved'
952 STATUS_APPROVED = 'approved'
955 STATUS_REJECTED = 'rejected'
953 STATUS_REJECTED = 'rejected'
956 STATUS_UNDER_REVIEW = 'under_review'
954 STATUS_UNDER_REVIEW = 'under_review'
957
955
958 STATUSES = [
956 STATUSES = [
959 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
957 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
960 (STATUS_APPROVED, _("Approved")),
958 (STATUS_APPROVED, _("Approved")),
961 (STATUS_REJECTED, _("Rejected")),
959 (STATUS_REJECTED, _("Rejected")),
962 (STATUS_UNDER_REVIEW, _("Under Review")),
960 (STATUS_UNDER_REVIEW, _("Under Review")),
963 ]
961 ]
964
962
965 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
963 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
966 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
964 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
965 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
968 revision = Column('revision', String(40), nullable=False)
966 revision = Column('revision', String(40), nullable=False)
969 status = Column('status', String(128), nullable=False, default=DEFAULT)
967 status = Column('status', String(128), nullable=False, default=DEFAULT)
970 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
968 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
971 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
969 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
972 version = Column('version', Integer(), nullable=False, default=0)
970 version = Column('version', Integer(), nullable=False, default=0)
973 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
971 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
974
972
975 author = relationship('User', lazy='joined')
973 author = relationship('User', lazy='joined')
976 repo = relationship('Repository')
974 repo = relationship('Repository')
977 comment = relationship('ChangesetComment', lazy='joined')
975 comment = relationship('ChangesetComment', lazy='joined')
978 pull_request = relationship('PullRequest', lazy='joined')
976 pull_request = relationship('PullRequest', lazy='joined')
979
977
980
978
981
979
982 class PullRequest(Base, BaseModel):
980 class PullRequest(Base, BaseModel):
983 __tablename__ = 'pull_requests'
981 __tablename__ = 'pull_requests'
984 __table_args__ = (
982 __table_args__ = (
985 {'extend_existing': True, 'mysql_engine': 'InnoDB',
983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
986 'mysql_charset': 'utf8'},
984 'mysql_charset': 'utf8'},
987 )
985 )
988
986
989 STATUS_NEW = u'new'
987 STATUS_NEW = u'new'
990 STATUS_OPEN = u'open'
988 STATUS_OPEN = u'open'
991 STATUS_CLOSED = u'closed'
989 STATUS_CLOSED = u'closed'
992
990
993 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
991 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
994 title = Column('title', Unicode(256), nullable=True)
992 title = Column('title', Unicode(256), nullable=True)
995 description = Column('description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
993 description = Column('description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
996 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
994 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
997 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
995 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
998 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
996 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
999 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
997 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1000 _revisions = Column('revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
998 _revisions = Column('revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
1001 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
999 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1002 org_ref = Column('org_ref', Unicode(256), nullable=False)
1000 org_ref = Column('org_ref', Unicode(256), nullable=False)
1003 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1001 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1004 other_ref = Column('other_ref', Unicode(256), nullable=False)
1002 other_ref = Column('other_ref', Unicode(256), nullable=False)
1005
1003
1006 author = relationship('User', lazy='joined')
1004 author = relationship('User', lazy='joined')
1007 reviewers = relationship('PullRequestReviewers',
1005 reviewers = relationship('PullRequestReviewers',
1008 cascade="all, delete, delete-orphan")
1006 cascade="all, delete, delete-orphan")
1009 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1007 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1010 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1008 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1011 statuses = relationship('ChangesetStatus')
1009 statuses = relationship('ChangesetStatus')
1012 comments = relationship('ChangesetComment',
1010 comments = relationship('ChangesetComment',
1013 cascade="all, delete, delete-orphan")
1011 cascade="all, delete, delete-orphan")
1014
1012
1015
1013
1016 class PullRequestReviewers(Base, BaseModel):
1014 class PullRequestReviewers(Base, BaseModel):
1017 __tablename__ = 'pull_request_reviewers'
1015 __tablename__ = 'pull_request_reviewers'
1018 __table_args__ = (
1016 __table_args__ = (
1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1020 'mysql_charset': 'utf8'},
1018 'mysql_charset': 'utf8'},
1021 )
1019 )
1022
1020
1023 def __init__(self, user=None, pull_request=None):
1021 def __init__(self, user=None, pull_request=None):
1024 self.user = user
1022 self.user = user
1025 self.pull_request = pull_request
1023 self.pull_request = pull_request
1026
1024
1027 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1025 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1028 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1026 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1027 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1030
1028
1031 user = relationship('User')
1029 user = relationship('User')
1032 pull_request = relationship('PullRequest')
1030 pull_request = relationship('PullRequest')
1033
1031
1034
1032
1035 class Notification(Base, BaseModel):
1033 class Notification(Base, BaseModel):
1036 __tablename__ = 'notifications'
1034 __tablename__ = 'notifications'
1037 __table_args__ = (
1035 __table_args__ = (
1038 Index('notification_type_idx', 'type'),
1036 Index('notification_type_idx', 'type'),
1039 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1037 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1040 'mysql_charset': 'utf8'},
1038 'mysql_charset': 'utf8'},
1041 )
1039 )
1042
1040
1043 TYPE_CHANGESET_COMMENT = u'cs_comment'
1041 TYPE_CHANGESET_COMMENT = u'cs_comment'
1044 TYPE_MESSAGE = u'message'
1042 TYPE_MESSAGE = u'message'
1045 TYPE_MENTION = u'mention'
1043 TYPE_MENTION = u'mention'
1046 TYPE_REGISTRATION = u'registration'
1044 TYPE_REGISTRATION = u'registration'
1047 TYPE_PULL_REQUEST = u'pull_request'
1045 TYPE_PULL_REQUEST = u'pull_request'
1048 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1046 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1049
1047
1050 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1048 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1051 subject = Column('subject', Unicode(512), nullable=True)
1049 subject = Column('subject', Unicode(512), nullable=True)
1052 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
1050 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
1053 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1051 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1054 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1052 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1055 type_ = Column('type', Unicode(256))
1053 type_ = Column('type', Unicode(256))
1056
1054
1057 created_by_user = relationship('User')
1055 created_by_user = relationship('User')
1058 notifications_to_users = relationship('UserNotification', lazy='joined',
1056 notifications_to_users = relationship('UserNotification', lazy='joined',
1059 cascade="all, delete, delete-orphan")
1057 cascade="all, delete, delete-orphan")
1060
1058
1061
1059
1062 class UserNotification(Base, BaseModel):
1060 class UserNotification(Base, BaseModel):
1063 __tablename__ = 'user_to_notification'
1061 __tablename__ = 'user_to_notification'
1064 __table_args__ = (
1062 __table_args__ = (
1065 UniqueConstraint('user_id', 'notification_id'),
1063 UniqueConstraint('user_id', 'notification_id'),
1066 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1064 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1067 'mysql_charset': 'utf8'}
1065 'mysql_charset': 'utf8'}
1068 )
1066 )
1069 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1067 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1070 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1068 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1071 read = Column('read', Boolean, default=False)
1069 read = Column('read', Boolean, default=False)
1072 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1070 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1073
1071
1074 user = relationship('User', lazy="joined")
1072 user = relationship('User', lazy="joined")
1075 notification = relationship('Notification', lazy="joined",
1073 notification = relationship('Notification', lazy="joined",
1076 order_by=lambda: Notification.created_on.desc(),)
1074 order_by=lambda: Notification.created_on.desc(),)
1077
1075
1078
1076
1079 class DbMigrateVersion(Base, BaseModel):
1077 class DbMigrateVersion(Base, BaseModel):
1080 __tablename__ = 'db_migrate_version'
1078 __tablename__ = 'db_migrate_version'
1081 __table_args__ = (
1079 __table_args__ = (
1082 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1080 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1083 'mysql_charset': 'utf8'},
1081 'mysql_charset': 'utf8'},
1084 )
1082 )
1085 repository_id = Column('repository_id', String(250), primary_key=True)
1083 repository_id = Column('repository_id', String(250), primary_key=True)
1086 repository_path = Column('repository_path', Text)
1084 repository_path = Column('repository_path', Text)
1087 version = Column('version', Integer)
1085 version = Column('version', Integer)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now