##// END OF EJS Templates
password-reset: strengthten security on password reset logic....
marcink -
r1471:9ea7077d default
parent child Browse files
Show More
@@ -0,0 +1,106 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.config.routing import ADMIN_PREFIX
24 from rhodecode.tests import (
25 TestController, clear_all_caches, url,
26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.utils import AssertResponse
29
30 fixture = Fixture()
31
32 # Hardcode URLs because we don't have a request object to use
33 # pyramids URL generation methods.
34 index_url = '/'
35 login_url = ADMIN_PREFIX + '/login'
36 logut_url = ADMIN_PREFIX + '/logout'
37 register_url = ADMIN_PREFIX + '/register'
38 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
39 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
40
41
42 class TestPasswordReset(TestController):
43
44 @pytest.mark.parametrize(
45 'pwd_reset_setting, show_link, show_reset', [
46 ('hg.password_reset.enabled', True, True),
47 ('hg.password_reset.hidden', False, True),
48 ('hg.password_reset.disabled', False, False),
49 ])
50 def test_password_reset_settings(
51 self, pwd_reset_setting, show_link, show_reset):
52 clear_all_caches()
53 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
54 params = {
55 'csrf_token': self.csrf_token,
56 'anonymous': 'True',
57 'default_register': 'hg.register.auto_activate',
58 'default_register_message': '',
59 'default_password_reset': pwd_reset_setting,
60 'default_extern_activate': 'hg.extern_activate.auto',
61 }
62 resp = self.app.post(url('admin_permissions_application'), params=params)
63 self.logout_user()
64
65 login_page = self.app.get(login_url)
66 asr_login = AssertResponse(login_page)
67 index_page = self.app.get(index_url)
68 asr_index = AssertResponse(index_page)
69
70 if show_link:
71 asr_login.one_element_exists('a.pwd_reset')
72 asr_index.one_element_exists('a.pwd_reset')
73 else:
74 asr_login.no_element_exists('a.pwd_reset')
75 asr_index.no_element_exists('a.pwd_reset')
76
77 response = self.app.get(pwd_reset_url)
78
79 assert_response = AssertResponse(response)
80 if show_reset:
81 response.mustcontain('Send password reset email')
82 assert_response.one_element_exists('#email')
83 assert_response.one_element_exists('#send')
84 else:
85 response.mustcontain('Password reset is disabled.')
86 assert_response.no_element_exists('#email')
87 assert_response.no_element_exists('#send')
88
89 def test_password_form_disabled(self):
90 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
91 params = {
92 'csrf_token': self.csrf_token,
93 'anonymous': 'True',
94 'default_register': 'hg.register.auto_activate',
95 'default_register_message': '',
96 'default_password_reset': 'hg.password_reset.disabled',
97 'default_extern_activate': 'hg.extern_activate.auto',
98 }
99 self.app.post(url('admin_permissions_application'), params=params)
100 self.logout_user()
101
102 response = self.app.post(
103 pwd_reset_url, {'email': 'lisa@rhodecode.com',}
104 )
105 response = response.follow()
106 response.mustcontain('Password reset is disabled.')
@@ -1,358 +1,390 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import time
21 22 import collections
22 23 import datetime
23 24 import formencode
24 25 import logging
25 26 import urlparse
26 27
27 28 from pylons import url
28 29 from pyramid.httpexceptions import HTTPFound
29 30 from pyramid.view import view_config
30 31 from recaptcha.client.captcha import submit
31 32
32 33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 34 from rhodecode.events import UserRegistered
34 35 from rhodecode.lib import helpers as h
35 36 from rhodecode.lib.auth import (
36 37 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
37 38 from rhodecode.lib.base import get_ip_addr
38 39 from rhodecode.lib.exceptions import UserCreationError
39 40 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
41 from rhodecode.model.db import User, UserApiKeys
41 42 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
42 43 from rhodecode.model.meta import Session
44 from rhodecode.model.auth_token import AuthTokenModel
43 45 from rhodecode.model.settings import SettingsModel
44 46 from rhodecode.model.user import UserModel
45 47 from rhodecode.translation import _
46 48
47 49
48 50 log = logging.getLogger(__name__)
49 51
50 52 CaptchaData = collections.namedtuple(
51 53 'CaptchaData', 'active, private_key, public_key')
52 54
53 55
54 56 def _store_user_in_session(session, username, remember=False):
55 57 user = User.get_by_username(username, case_insensitive=True)
56 58 auth_user = AuthUser(user.user_id)
57 59 auth_user.set_authenticated()
58 60 cs = auth_user.get_cookie_store()
59 61 session['rhodecode_user'] = cs
60 62 user.update_lastlogin()
61 63 Session().commit()
62 64
63 65 # If they want to be remembered, update the cookie
64 66 if remember:
65 67 _year = (datetime.datetime.now() +
66 68 datetime.timedelta(seconds=60 * 60 * 24 * 365))
67 69 session._set_cookie_expires(_year)
68 70
69 71 session.save()
70 72
71 73 safe_cs = cs.copy()
72 74 safe_cs['password'] = '****'
73 75 log.info('user %s is now authenticated and stored in '
74 76 'session, session attrs %s', username, safe_cs)
75 77
76 78 # dumps session attrs back to cookie
77 79 session._update_cookie_out()
78 80 # we set new cookie
79 81 headers = None
80 82 if session.request['set_cookie']:
81 83 # send set-cookie headers back to response to update cookie
82 84 headers = [('Set-Cookie', session.request['cookie_out'])]
83 85 return headers
84 86
85 87
86 88 def get_came_from(request):
87 89 came_from = safe_str(request.GET.get('came_from', ''))
88 90 parsed = urlparse.urlparse(came_from)
89 91 allowed_schemes = ['http', 'https']
90 92 if parsed.scheme and parsed.scheme not in allowed_schemes:
91 93 log.error('Suspicious URL scheme detected %s for url %s' %
92 94 (parsed.scheme, parsed))
93 95 came_from = url('home')
94 96 elif parsed.netloc and request.host != parsed.netloc:
95 97 log.error('Suspicious NETLOC detected %s for url %s server url '
96 98 'is: %s' % (parsed.netloc, parsed, request.host))
97 99 came_from = url('home')
98 100 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
99 101 log.error('Header injection detected `%s` for url %s server url ' %
100 102 (parsed.path, parsed))
101 103 came_from = url('home')
102 104
103 105 return came_from or url('home')
104 106
105 107
106 108 class LoginView(object):
107 109
108 110 def __init__(self, context, request):
109 111 self.request = request
110 112 self.context = context
111 113 self.session = request.session
112 114 self._rhodecode_user = request.user
113 115
114 116 def _get_template_context(self):
115 117 return {
116 118 'came_from': get_came_from(self.request),
117 119 'defaults': {},
118 120 'errors': {},
119 121 }
120 122
121 123 def _get_captcha_data(self):
122 124 settings = SettingsModel().get_all_settings()
123 125 private_key = settings.get('rhodecode_captcha_private_key')
124 126 public_key = settings.get('rhodecode_captcha_public_key')
125 127 active = bool(private_key)
126 128 return CaptchaData(
127 129 active=active, private_key=private_key, public_key=public_key)
128 130
129 131 @view_config(
130 132 route_name='login', request_method='GET',
131 133 renderer='rhodecode:templates/login.mako')
132 134 def login(self):
133 135 came_from = get_came_from(self.request)
134 136 user = self.request.user
135 137
136 138 # redirect if already logged in
137 139 if user.is_authenticated and not user.is_default and user.ip_allowed:
138 140 raise HTTPFound(came_from)
139 141
140 142 # check if we use headers plugin, and try to login using it.
141 143 try:
142 144 log.debug('Running PRE-AUTH for headers based authentication')
143 145 auth_info = authenticate(
144 146 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
145 147 if auth_info:
146 148 headers = _store_user_in_session(
147 149 self.session, auth_info.get('username'))
148 150 raise HTTPFound(came_from, headers=headers)
149 151 except UserCreationError as e:
150 152 log.error(e)
151 153 self.session.flash(e, queue='error')
152 154
153 155 return self._get_template_context()
154 156
155 157 @view_config(
156 158 route_name='login', request_method='POST',
157 159 renderer='rhodecode:templates/login.mako')
158 160 def login_post(self):
159 161 came_from = get_came_from(self.request)
160 162
161 163 login_form = LoginForm()()
162 164
163 165 try:
164 166 self.session.invalidate()
165 167 form_result = login_form.to_python(self.request.params)
166 168 # form checks for username/password, now we're authenticated
167 169 headers = _store_user_in_session(
168 170 self.session,
169 171 username=form_result['username'],
170 172 remember=form_result['remember'])
171 173 log.debug('Redirecting to "%s" after login.', came_from)
172 174 raise HTTPFound(came_from, headers=headers)
173 175 except formencode.Invalid as errors:
174 176 defaults = errors.value
175 177 # remove password from filling in form again
176 178 defaults.pop('password', None)
177 179 render_ctx = self._get_template_context()
178 180 render_ctx.update({
179 181 'errors': errors.error_dict,
180 182 'defaults': defaults,
181 183 })
182 184 return render_ctx
183 185
184 186 except UserCreationError as e:
185 187 # headers auth or other auth functions that create users on
186 188 # the fly can throw this exception signaling that there's issue
187 189 # with user creation, explanation should be provided in
188 190 # Exception itself
189 191 self.session.flash(e, queue='error')
190 192 return self._get_template_context()
191 193
192 194 @CSRFRequired()
193 195 @view_config(route_name='logout', request_method='POST')
194 196 def logout(self):
195 197 user = self.request.user
196 198 log.info('Deleting session for user: `%s`', user)
197 199 self.session.delete()
198 200 return HTTPFound(url('home'))
199 201
200 202 @HasPermissionAnyDecorator(
201 203 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
202 204 @view_config(
203 205 route_name='register', request_method='GET',
204 206 renderer='rhodecode:templates/register.mako',)
205 207 def register(self, defaults=None, errors=None):
206 208 defaults = defaults or {}
207 209 errors = errors or {}
208 210
209 211 settings = SettingsModel().get_all_settings()
210 212 register_message = settings.get('rhodecode_register_message') or ''
211 213 captcha = self._get_captcha_data()
212 214 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
213 215 .AuthUser.permissions['global']
214 216
215 217 render_ctx = self._get_template_context()
216 218 render_ctx.update({
217 219 'defaults': defaults,
218 220 'errors': errors,
219 221 'auto_active': auto_active,
220 222 'captcha_active': captcha.active,
221 223 'captcha_public_key': captcha.public_key,
222 224 'register_message': register_message,
223 225 })
224 226 return render_ctx
225 227
226 228 @HasPermissionAnyDecorator(
227 229 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
228 230 @view_config(
229 231 route_name='register', request_method='POST',
230 232 renderer='rhodecode:templates/register.mako')
231 233 def register_post(self):
232 234 captcha = self._get_captcha_data()
233 235 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 236 .AuthUser.permissions['global']
235 237
236 238 register_form = RegisterForm()()
237 239 try:
238 240 form_result = register_form.to_python(self.request.params)
239 241 form_result['active'] = auto_active
240 242
241 243 if captcha.active:
242 244 response = submit(
243 245 self.request.params.get('recaptcha_challenge_field'),
244 246 self.request.params.get('recaptcha_response_field'),
245 247 private_key=captcha.private_key,
246 248 remoteip=get_ip_addr(self.request.environ))
247 249 if not response.is_valid:
248 250 _value = form_result
249 251 _msg = _('Bad captcha')
250 252 error_dict = {'recaptcha_field': _msg}
251 253 raise formencode.Invalid(_msg, _value, None,
252 254 error_dict=error_dict)
253 255
254 256 new_user = UserModel().create_registration(form_result)
255 257 event = UserRegistered(user=new_user, session=self.session)
256 258 self.request.registry.notify(event)
257 259 self.session.flash(
258 260 _('You have successfully registered with RhodeCode'),
259 261 queue='success')
260 262 Session().commit()
261 263
262 264 redirect_ro = self.request.route_path('login')
263 265 raise HTTPFound(redirect_ro)
264 266
265 267 except formencode.Invalid as errors:
266 268 errors.value.pop('password', None)
267 269 errors.value.pop('password_confirmation', None)
268 270 return self.register(
269 271 defaults=errors.value, errors=errors.error_dict)
270 272
271 273 except UserCreationError as e:
272 274 # container auth or other auth functions that create users on
273 275 # the fly can throw this exception signaling that there's issue
274 276 # with user creation, explanation should be provided in
275 277 # Exception itself
276 278 self.session.flash(e, queue='error')
277 279 return self.register()
278 280
279 281 @view_config(
280 282 route_name='reset_password', request_method=('GET', 'POST'),
281 283 renderer='rhodecode:templates/password_reset.mako')
282 284 def password_reset(self):
283 285 captcha = self._get_captcha_data()
284 286
285 287 render_ctx = {
286 288 'captcha_active': captcha.active,
287 289 'captcha_public_key': captcha.public_key,
288 290 'defaults': {},
289 291 'errors': {},
290 292 }
291 293
294 # always send implicit message to prevent from discovery of
295 # matching emails
296 msg = _('If such email exists, a password reset link was sent to it.')
297
292 298 if self.request.POST:
299 if h.HasPermissionAny('hg.password_reset.disabled')():
300 _email = self.request.POST.get('email', '')
301 log.error('Failed attempt to reset password for `%s`.', _email)
302 self.session.flash(_('Password reset has been disabled.'),
303 queue='error')
304 return HTTPFound(self.request.route_path('reset_password'))
305
293 306 password_reset_form = PasswordResetForm()()
294 307 try:
295 308 form_result = password_reset_form.to_python(
296 309 self.request.params)
297 if h.HasPermissionAny('hg.password_reset.disabled')():
298 log.error('Failed attempt to reset password for %s.', form_result['email'] )
299 self.session.flash(
300 _('Password reset has been disabled.'),
301 queue='error')
302 return HTTPFound(self.request.route_path('reset_password'))
310 user_email = form_result['email']
311
303 312 if captcha.active:
304 313 response = submit(
305 314 self.request.params.get('recaptcha_challenge_field'),
306 315 self.request.params.get('recaptcha_response_field'),
307 316 private_key=captcha.private_key,
308 317 remoteip=get_ip_addr(self.request.environ))
309 318 if not response.is_valid:
310 319 _value = form_result
311 320 _msg = _('Bad captcha')
312 321 error_dict = {'recaptcha_field': _msg}
313 raise formencode.Invalid(_msg, _value, None,
314 error_dict=error_dict)
322 raise formencode.Invalid(
323 _msg, _value, None, error_dict=error_dict)
324 # Generate reset URL and send mail.
325 user = User.get_by_email(user_email)
315 326
316 # Generate reset URL and send mail.
317 user_email = form_result['email']
318 user = User.get_by_email(user_email)
327 # generate password reset token that expires in 10minutes
328 desc = 'Generated token for password reset from {}'.format(
329 datetime.datetime.now().isoformat())
330 reset_token = AuthTokenModel().create(
331 user, lifetime=10,
332 description=desc,
333 role=UserApiKeys.ROLE_PASSWORD_RESET)
334 Session().commit()
335
336 log.debug('Successfully created password recovery token')
319 337 password_reset_url = self.request.route_url(
320 338 'reset_password_confirmation',
321 _query={'key': user.api_key})
339 _query={'key': reset_token.api_key})
322 340 UserModel().reset_password_link(
323 341 form_result, password_reset_url)
324
325 342 # Display success message and redirect.
326 self.session.flash(
327 _('Your password reset link was sent'),
328 queue='success')
329 return HTTPFound(self.request.route_path('login'))
343 self.session.flash(msg, queue='success')
344 return HTTPFound(self.request.route_path('reset_password'))
330 345
331 346 except formencode.Invalid as errors:
332 347 render_ctx.update({
333 348 'defaults': errors.value,
334 'errors': errors.error_dict,
335 349 })
350 log.debug('faking response on invalid password reset')
351 # make this take 2s, to prevent brute forcing.
352 time.sleep(2)
353 self.session.flash(msg, queue='success')
354 return HTTPFound(self.request.route_path('reset_password'))
336 355
337 356 return render_ctx
338 357
339 358 @view_config(route_name='reset_password_confirmation',
340 359 request_method='GET')
341 360 def password_reset_confirmation(self):
361
342 362 if self.request.GET and self.request.GET.get('key'):
363 # make this take 2s, to prevent brute forcing.
364 time.sleep(2)
365
366 token = AuthTokenModel().get_auth_token(
367 self.request.GET.get('key'))
368
369 # verify token is the correct role
370 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
371 log.debug('Got token with role:%s expected is %s',
372 getattr(token, 'role', 'EMPTY_TOKEN'),
373 UserApiKeys.ROLE_PASSWORD_RESET)
374 self.session.flash(
375 _('Given reset token is invalid'), queue='error')
376 return HTTPFound(self.request.route_path('reset_password'))
377
343 378 try:
344 user = User.get_by_auth_token(self.request.GET.get('key'))
345 password_reset_url = self.request.route_url(
346 'reset_password_confirmation',
347 _query={'key': user.api_key})
348 data = {'email': user.email}
349 UserModel().reset_password(data, password_reset_url)
379 owner = token.user
380 data = {'email': owner.email, 'token': token.api_key}
381 UserModel().reset_password(data)
350 382 self.session.flash(
351 383 _('Your password reset was successful, '
352 384 'a new password has been sent to your email'),
353 385 queue='success')
354 386 except Exception as e:
355 387 log.error(e)
356 388 return HTTPFound(self.request.route_path('reset_password'))
357 389
358 390 return HTTPFound(self.request.route_path('login'))
@@ -1,87 +1,97 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 authentication tokens model for RhodeCode
23 23 """
24 24
25 25 import time
26 26 import logging
27 27 import traceback
28 28 from sqlalchemy import or_
29 29
30 30 from rhodecode.model import BaseModel
31 31 from rhodecode.model.db import UserApiKeys
32 32 from rhodecode.model.meta import Session
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AuthTokenModel(BaseModel):
38 38 cls = UserApiKeys
39 39
40 40 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
41 41 """
42 42 :param user: user or user_id
43 43 :param description: description of ApiKey
44 :param lifetime: expiration time in seconds
44 :param lifetime: expiration time in minutes
45 45 :param role: role for the apikey
46 46 """
47 47 from rhodecode.lib.auth import generate_auth_token
48 48
49 49 user = self._get_user(user)
50 50
51 51 new_auth_token = UserApiKeys()
52 52 new_auth_token.api_key = generate_auth_token(user.username)
53 53 new_auth_token.user_id = user.user_id
54 54 new_auth_token.description = description
55 55 new_auth_token.role = role
56 56 new_auth_token.expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
57 57 Session().add(new_auth_token)
58 58
59 59 return new_auth_token
60 60
61 61 def delete(self, api_key, user=None):
62 62 """
63 63 Deletes given api_key, if user is set it also filters the object for
64 64 deletion by given user.
65 65 """
66 66 api_key = UserApiKeys.query().filter(UserApiKeys.api_key == api_key)
67 67
68 68 if user:
69 69 user = self._get_user(user)
70 70 api_key = api_key.filter(UserApiKeys.user_id == user.user_id)
71 71
72 72 api_key = api_key.scalar()
73 73 try:
74 74 Session().delete(api_key)
75 75 except Exception:
76 76 log.error(traceback.format_exc())
77 77 raise
78 78
79 79 def get_auth_tokens(self, user, show_expired=True):
80 80 user = self._get_user(user)
81 81 user_auth_tokens = UserApiKeys.query()\
82 82 .filter(UserApiKeys.user_id == user.user_id)
83 83 if not show_expired:
84 84 user_auth_tokens = user_auth_tokens\
85 85 .filter(or_(UserApiKeys.expires == -1,
86 86 UserApiKeys.expires >= time.time()))
87 87 return user_auth_tokens
88
89 def get_auth_token(self, auth_token):
90 auth_token = UserApiKeys.query().filter(
91 UserApiKeys.api_key == auth_token)
92 auth_token = auth_token \
93 .filter(or_(UserApiKeys.expires == -1,
94 UserApiKeys.expires >= time.time()))\
95 .first()
96
97 return auth_token
@@ -1,3909 +1,3911 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict, cleaned_uri)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 354 HOOK_PUSH = 'changegroup.push_logger'
355 355
356 356 # TODO: johbo: Unify way how hooks are configured for git and hg,
357 357 # git part is currently hardcoded.
358 358
359 359 # SVN PATTERNS
360 360 SVN_BRANCH_ID = 'vcs_svn_branch'
361 361 SVN_TAG_ID = 'vcs_svn_tag'
362 362
363 363 ui_id = Column(
364 364 "ui_id", Integer(), nullable=False, unique=True, default=None,
365 365 primary_key=True)
366 366 ui_section = Column(
367 367 "ui_section", String(255), nullable=True, unique=None, default=None)
368 368 ui_key = Column(
369 369 "ui_key", String(255), nullable=True, unique=None, default=None)
370 370 ui_value = Column(
371 371 "ui_value", String(255), nullable=True, unique=None, default=None)
372 372 ui_active = Column(
373 373 "ui_active", Boolean(), nullable=True, unique=None, default=True)
374 374
375 375 def __repr__(self):
376 376 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
377 377 self.ui_key, self.ui_value)
378 378
379 379
380 380 class RepoRhodeCodeSetting(Base, BaseModel):
381 381 __tablename__ = 'repo_rhodecode_settings'
382 382 __table_args__ = (
383 383 UniqueConstraint(
384 384 'app_settings_name', 'repository_id',
385 385 name='uq_repo_rhodecode_setting_name_repo_id'),
386 386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 388 )
389 389
390 390 repository_id = Column(
391 391 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
392 392 nullable=False)
393 393 app_settings_id = Column(
394 394 "app_settings_id", Integer(), nullable=False, unique=True,
395 395 default=None, primary_key=True)
396 396 app_settings_name = Column(
397 397 "app_settings_name", String(255), nullable=True, unique=None,
398 398 default=None)
399 399 _app_settings_value = Column(
400 400 "app_settings_value", String(4096), nullable=True, unique=None,
401 401 default=None)
402 402 _app_settings_type = Column(
403 403 "app_settings_type", String(255), nullable=True, unique=None,
404 404 default=None)
405 405
406 406 repository = relationship('Repository')
407 407
408 408 def __init__(self, repository_id, key='', val='', type='unicode'):
409 409 self.repository_id = repository_id
410 410 self.app_settings_name = key
411 411 self.app_settings_type = type
412 412 self.app_settings_value = val
413 413
414 414 @validates('_app_settings_value')
415 415 def validate_settings_value(self, key, val):
416 416 assert type(val) == unicode
417 417 return val
418 418
419 419 @hybrid_property
420 420 def app_settings_value(self):
421 421 v = self._app_settings_value
422 422 type_ = self.app_settings_type
423 423 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
424 424 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
425 425 return converter(v)
426 426
427 427 @app_settings_value.setter
428 428 def app_settings_value(self, val):
429 429 """
430 430 Setter that will always make sure we use unicode in app_settings_value
431 431
432 432 :param val:
433 433 """
434 434 self._app_settings_value = safe_unicode(val)
435 435
436 436 @hybrid_property
437 437 def app_settings_type(self):
438 438 return self._app_settings_type
439 439
440 440 @app_settings_type.setter
441 441 def app_settings_type(self, val):
442 442 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
443 443 if val not in SETTINGS_TYPES:
444 444 raise Exception('type must be one of %s got %s'
445 445 % (SETTINGS_TYPES.keys(), val))
446 446 self._app_settings_type = val
447 447
448 448 def __unicode__(self):
449 449 return u"<%s('%s:%s:%s[%s]')>" % (
450 450 self.__class__.__name__, self.repository.repo_name,
451 451 self.app_settings_name, self.app_settings_value,
452 452 self.app_settings_type
453 453 )
454 454
455 455
456 456 class RepoRhodeCodeUi(Base, BaseModel):
457 457 __tablename__ = 'repo_rhodecode_ui'
458 458 __table_args__ = (
459 459 UniqueConstraint(
460 460 'repository_id', 'ui_section', 'ui_key',
461 461 name='uq_repo_rhodecode_ui_repository_id_section_key'),
462 462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
463 463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
464 464 )
465 465
466 466 repository_id = Column(
467 467 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
468 468 nullable=False)
469 469 ui_id = Column(
470 470 "ui_id", Integer(), nullable=False, unique=True, default=None,
471 471 primary_key=True)
472 472 ui_section = Column(
473 473 "ui_section", String(255), nullable=True, unique=None, default=None)
474 474 ui_key = Column(
475 475 "ui_key", String(255), nullable=True, unique=None, default=None)
476 476 ui_value = Column(
477 477 "ui_value", String(255), nullable=True, unique=None, default=None)
478 478 ui_active = Column(
479 479 "ui_active", Boolean(), nullable=True, unique=None, default=True)
480 480
481 481 repository = relationship('Repository')
482 482
483 483 def __repr__(self):
484 484 return '<%s[%s:%s]%s=>%s]>' % (
485 485 self.__class__.__name__, self.repository.repo_name,
486 486 self.ui_section, self.ui_key, self.ui_value)
487 487
488 488
489 489 class User(Base, BaseModel):
490 490 __tablename__ = 'users'
491 491 __table_args__ = (
492 492 UniqueConstraint('username'), UniqueConstraint('email'),
493 493 Index('u_username_idx', 'username'),
494 494 Index('u_email_idx', 'email'),
495 495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
496 496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
497 497 )
498 498 DEFAULT_USER = 'default'
499 499 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
500 500 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
501 501
502 502 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
503 503 username = Column("username", String(255), nullable=True, unique=None, default=None)
504 504 password = Column("password", String(255), nullable=True, unique=None, default=None)
505 505 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
506 506 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
507 507 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
508 508 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
509 509 _email = Column("email", String(255), nullable=True, unique=None, default=None)
510 510 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
511 511 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
512 512 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
513 513 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
514 514 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
515 515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
516 516 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
517 517
518 518 user_log = relationship('UserLog')
519 519 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
520 520
521 521 repositories = relationship('Repository')
522 522 repository_groups = relationship('RepoGroup')
523 523 user_groups = relationship('UserGroup')
524 524
525 525 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
526 526 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
527 527
528 528 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
529 529 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
530 530 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
531 531
532 532 group_member = relationship('UserGroupMember', cascade='all')
533 533
534 534 notifications = relationship('UserNotification', cascade='all')
535 535 # notifications assigned to this user
536 536 user_created_notifications = relationship('Notification', cascade='all')
537 537 # comments created by this user
538 538 user_comments = relationship('ChangesetComment', cascade='all')
539 539 # user profile extra info
540 540 user_emails = relationship('UserEmailMap', cascade='all')
541 541 user_ip_map = relationship('UserIpMap', cascade='all')
542 542 user_auth_tokens = relationship('UserApiKeys', cascade='all')
543 543 # gists
544 544 user_gists = relationship('Gist', cascade='all')
545 545 # user pull requests
546 546 user_pull_requests = relationship('PullRequest', cascade='all')
547 547 # external identities
548 548 extenal_identities = relationship(
549 549 'ExternalIdentity',
550 550 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
551 551 cascade='all')
552 552
553 553 def __unicode__(self):
554 554 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
555 555 self.user_id, self.username)
556 556
557 557 @hybrid_property
558 558 def email(self):
559 559 return self._email
560 560
561 561 @email.setter
562 562 def email(self, val):
563 563 self._email = val.lower() if val else None
564 564
565 565 @property
566 566 def firstname(self):
567 567 # alias for future
568 568 return self.name
569 569
570 570 @property
571 571 def emails(self):
572 572 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
573 573 return [self.email] + [x.email for x in other]
574 574
575 575 @property
576 576 def auth_tokens(self):
577 577 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
578 578
579 579 @property
580 580 def extra_auth_tokens(self):
581 581 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
582 582
583 583 @property
584 584 def feed_token(self):
585 585 return self.get_feed_token()
586 586
587 587 def get_feed_token(self):
588 588 feed_tokens = UserApiKeys.query()\
589 589 .filter(UserApiKeys.user == self)\
590 590 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
591 591 .all()
592 592 if feed_tokens:
593 593 return feed_tokens[0].api_key
594 594 return 'NO_FEED_TOKEN_AVAILABLE'
595 595
596 596 @classmethod
597 597 def extra_valid_auth_tokens(cls, user, role=None):
598 598 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
599 599 .filter(or_(UserApiKeys.expires == -1,
600 600 UserApiKeys.expires >= time.time()))
601 601 if role:
602 602 tokens = tokens.filter(or_(UserApiKeys.role == role,
603 603 UserApiKeys.role == UserApiKeys.ROLE_ALL))
604 604 return tokens.all()
605 605
606 606 def authenticate_by_token(self, auth_token, roles=None,
607 607 include_builtin_token=False):
608 608 from rhodecode.lib import auth
609 609
610 610 log.debug('Trying to authenticate user: %s via auth-token, '
611 611 'and roles: %s', self, roles)
612 612
613 613 if not auth_token:
614 614 return False
615 615
616 616 crypto_backend = auth.crypto_backend()
617 617
618 618 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
619 619 tokens_q = UserApiKeys.query()\
620 620 .filter(UserApiKeys.user_id == self.user_id)\
621 621 .filter(or_(UserApiKeys.expires == -1,
622 622 UserApiKeys.expires >= time.time()))
623 623
624 624 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
625 625
626 626 maybe_builtin = []
627 627 if include_builtin_token:
628 628 maybe_builtin = [AttributeDict({'api_key': self.api_key})]
629 629
630 630 plain_tokens = []
631 631 hash_tokens = []
632 632
633 633 for token in tokens_q.all() + maybe_builtin:
634 634 if token.api_key.startswith(crypto_backend.ENC_PREF):
635 635 hash_tokens.append(token.api_key)
636 636 else:
637 637 plain_tokens.append(token.api_key)
638 638
639 639 is_plain_match = auth_token in plain_tokens
640 640 if is_plain_match:
641 641 return True
642 642
643 643 for hashed in hash_tokens:
644 644 # marcink: this is expensive to calculate, but the most secure
645 645 match = crypto_backend.hash_check(auth_token, hashed)
646 646 if match:
647 647 return True
648 648
649 649 return False
650 650
651 651 @property
652 652 def builtin_token_roles(self):
653 653 roles = [
654 654 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
655 655 ]
656 656 return map(UserApiKeys._get_role_name, roles)
657 657
658 658 @property
659 659 def ip_addresses(self):
660 660 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
661 661 return [x.ip_addr for x in ret]
662 662
663 663 @property
664 664 def username_and_name(self):
665 665 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
666 666
667 667 @property
668 668 def username_or_name_or_email(self):
669 669 full_name = self.full_name if self.full_name is not ' ' else None
670 670 return self.username or full_name or self.email
671 671
672 672 @property
673 673 def full_name(self):
674 674 return '%s %s' % (self.firstname, self.lastname)
675 675
676 676 @property
677 677 def full_name_or_username(self):
678 678 return ('%s %s' % (self.firstname, self.lastname)
679 679 if (self.firstname and self.lastname) else self.username)
680 680
681 681 @property
682 682 def full_contact(self):
683 683 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
684 684
685 685 @property
686 686 def short_contact(self):
687 687 return '%s %s' % (self.firstname, self.lastname)
688 688
689 689 @property
690 690 def is_admin(self):
691 691 return self.admin
692 692
693 693 @property
694 694 def AuthUser(self):
695 695 """
696 696 Returns instance of AuthUser for this user
697 697 """
698 698 from rhodecode.lib.auth import AuthUser
699 699 return AuthUser(user_id=self.user_id, api_key=self.api_key,
700 700 username=self.username)
701 701
702 702 @hybrid_property
703 703 def user_data(self):
704 704 if not self._user_data:
705 705 return {}
706 706
707 707 try:
708 708 return json.loads(self._user_data)
709 709 except TypeError:
710 710 return {}
711 711
712 712 @user_data.setter
713 713 def user_data(self, val):
714 714 if not isinstance(val, dict):
715 715 raise Exception('user_data must be dict, got %s' % type(val))
716 716 try:
717 717 self._user_data = json.dumps(val)
718 718 except Exception:
719 719 log.error(traceback.format_exc())
720 720
721 721 @classmethod
722 722 def get_by_username(cls, username, case_insensitive=False,
723 723 cache=False, identity_cache=False):
724 724 session = Session()
725 725
726 726 if case_insensitive:
727 727 q = cls.query().filter(
728 728 func.lower(cls.username) == func.lower(username))
729 729 else:
730 730 q = cls.query().filter(cls.username == username)
731 731
732 732 if cache:
733 733 if identity_cache:
734 734 val = cls.identity_cache(session, 'username', username)
735 735 if val:
736 736 return val
737 737 else:
738 738 q = q.options(
739 739 FromCache("sql_cache_short",
740 740 "get_user_by_name_%s" % _hash_key(username)))
741 741
742 742 return q.scalar()
743 743
744 744 @classmethod
745 745 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
746 746 q = cls.query().filter(cls.api_key == auth_token)
747 747
748 748 if cache:
749 749 q = q.options(FromCache("sql_cache_short",
750 750 "get_auth_token_%s" % auth_token))
751 751 res = q.scalar()
752 752
753 753 if fallback and not res:
754 754 #fallback to additional keys
755 755 _res = UserApiKeys.query()\
756 756 .filter(UserApiKeys.api_key == auth_token)\
757 757 .filter(or_(UserApiKeys.expires == -1,
758 758 UserApiKeys.expires >= time.time()))\
759 759 .first()
760 760 if _res:
761 761 res = _res.user
762 762 return res
763 763
764 764 @classmethod
765 765 def get_by_email(cls, email, case_insensitive=False, cache=False):
766 766
767 767 if case_insensitive:
768 768 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
769 769
770 770 else:
771 771 q = cls.query().filter(cls.email == email)
772 772
773 773 if cache:
774 774 q = q.options(FromCache("sql_cache_short",
775 775 "get_email_key_%s" % _hash_key(email)))
776 776
777 777 ret = q.scalar()
778 778 if ret is None:
779 779 q = UserEmailMap.query()
780 780 # try fetching in alternate email map
781 781 if case_insensitive:
782 782 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
783 783 else:
784 784 q = q.filter(UserEmailMap.email == email)
785 785 q = q.options(joinedload(UserEmailMap.user))
786 786 if cache:
787 787 q = q.options(FromCache("sql_cache_short",
788 788 "get_email_map_key_%s" % email))
789 789 ret = getattr(q.scalar(), 'user', None)
790 790
791 791 return ret
792 792
793 793 @classmethod
794 794 def get_from_cs_author(cls, author):
795 795 """
796 796 Tries to get User objects out of commit author string
797 797
798 798 :param author:
799 799 """
800 800 from rhodecode.lib.helpers import email, author_name
801 801 # Valid email in the attribute passed, see if they're in the system
802 802 _email = email(author)
803 803 if _email:
804 804 user = cls.get_by_email(_email, case_insensitive=True)
805 805 if user:
806 806 return user
807 807 # Maybe we can match by username?
808 808 _author = author_name(author)
809 809 user = cls.get_by_username(_author, case_insensitive=True)
810 810 if user:
811 811 return user
812 812
813 813 def update_userdata(self, **kwargs):
814 814 usr = self
815 815 old = usr.user_data
816 816 old.update(**kwargs)
817 817 usr.user_data = old
818 818 Session().add(usr)
819 819 log.debug('updated userdata with ', kwargs)
820 820
821 821 def update_lastlogin(self):
822 822 """Update user lastlogin"""
823 823 self.last_login = datetime.datetime.now()
824 824 Session().add(self)
825 825 log.debug('updated user %s lastlogin', self.username)
826 826
827 827 def update_lastactivity(self):
828 828 """Update user lastactivity"""
829 829 usr = self
830 830 old = usr.user_data
831 831 old.update({'last_activity': time.time()})
832 832 usr.user_data = old
833 833 Session().add(usr)
834 834 log.debug('updated user %s lastactivity', usr.username)
835 835
836 836 def update_password(self, new_password, change_api_key=False):
837 837 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
838 838
839 839 self.password = get_crypt_password(new_password)
840 840 if change_api_key:
841 841 self.api_key = generate_auth_token(self.username)
842 842 Session().add(self)
843 843
844 844 @classmethod
845 845 def get_first_super_admin(cls):
846 846 user = User.query().filter(User.admin == true()).first()
847 847 if user is None:
848 848 raise Exception('FATAL: Missing administrative account!')
849 849 return user
850 850
851 851 @classmethod
852 852 def get_all_super_admins(cls):
853 853 """
854 854 Returns all admin accounts sorted by username
855 855 """
856 856 return User.query().filter(User.admin == true())\
857 857 .order_by(User.username.asc()).all()
858 858
859 859 @classmethod
860 860 def get_default_user(cls, cache=False):
861 861 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
862 862 if user is None:
863 863 raise Exception('FATAL: Missing default account!')
864 864 return user
865 865
866 866 def _get_default_perms(self, user, suffix=''):
867 867 from rhodecode.model.permission import PermissionModel
868 868 return PermissionModel().get_default_perms(user.user_perms, suffix)
869 869
870 870 def get_default_perms(self, suffix=''):
871 871 return self._get_default_perms(self, suffix)
872 872
873 873 def get_api_data(self, include_secrets=False, details='full'):
874 874 """
875 875 Common function for generating user related data for API
876 876
877 877 :param include_secrets: By default secrets in the API data will be replaced
878 878 by a placeholder value to prevent exposing this data by accident. In case
879 879 this data shall be exposed, set this flag to ``True``.
880 880
881 881 :param details: details can be 'basic|full' basic gives only a subset of
882 882 the available user information that includes user_id, name and emails.
883 883 """
884 884 user = self
885 885 user_data = self.user_data
886 886 data = {
887 887 'user_id': user.user_id,
888 888 'username': user.username,
889 889 'firstname': user.name,
890 890 'lastname': user.lastname,
891 891 'email': user.email,
892 892 'emails': user.emails,
893 893 }
894 894 if details == 'basic':
895 895 return data
896 896
897 897 api_key_length = 40
898 898 api_key_replacement = '*' * api_key_length
899 899
900 900 extras = {
901 901 'api_key': api_key_replacement,
902 902 'api_keys': [api_key_replacement],
903 903 'active': user.active,
904 904 'admin': user.admin,
905 905 'extern_type': user.extern_type,
906 906 'extern_name': user.extern_name,
907 907 'last_login': user.last_login,
908 908 'ip_addresses': user.ip_addresses,
909 909 'language': user_data.get('language')
910 910 }
911 911 data.update(extras)
912 912
913 913 if include_secrets:
914 914 data['api_key'] = user.api_key
915 915 data['api_keys'] = user.auth_tokens
916 916 return data
917 917
918 918 def __json__(self):
919 919 data = {
920 920 'full_name': self.full_name,
921 921 'full_name_or_username': self.full_name_or_username,
922 922 'short_contact': self.short_contact,
923 923 'full_contact': self.full_contact,
924 924 }
925 925 data.update(self.get_api_data())
926 926 return data
927 927
928 928
929 929 class UserApiKeys(Base, BaseModel):
930 930 __tablename__ = 'user_api_keys'
931 931 __table_args__ = (
932 932 Index('uak_api_key_idx', 'api_key'),
933 933 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
934 934 UniqueConstraint('api_key'),
935 935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 937 )
938 938 __mapper_args__ = {}
939 939
940 940 # ApiKey role
941 941 ROLE_ALL = 'token_role_all'
942 942 ROLE_HTTP = 'token_role_http'
943 943 ROLE_VCS = 'token_role_vcs'
944 944 ROLE_API = 'token_role_api'
945 945 ROLE_FEED = 'token_role_feed'
946 ROLE_PASSWORD_RESET = 'token_password_reset'
947
946 948 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
947 949
948 950 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
949 951 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
950 952 api_key = Column("api_key", String(255), nullable=False, unique=True)
951 953 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
952 954 expires = Column('expires', Float(53), nullable=False)
953 955 role = Column('role', String(255), nullable=True)
954 956 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
955 957
956 958 user = relationship('User', lazy='joined')
957 959
958 960 @classmethod
959 961 def _get_role_name(cls, role):
960 962 return {
961 963 cls.ROLE_ALL: _('all'),
962 964 cls.ROLE_HTTP: _('http/web interface'),
963 965 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
964 966 cls.ROLE_API: _('api calls'),
965 967 cls.ROLE_FEED: _('feed access'),
966 968 }.get(role, role)
967 969
968 970 @property
969 971 def expired(self):
970 972 if self.expires == -1:
971 973 return False
972 974 return time.time() > self.expires
973 975
974 976 @property
975 977 def role_humanized(self):
976 978 return self._get_role_name(self.role)
977 979
978 980
979 981 class UserEmailMap(Base, BaseModel):
980 982 __tablename__ = 'user_email_map'
981 983 __table_args__ = (
982 984 Index('uem_email_idx', 'email'),
983 985 UniqueConstraint('email'),
984 986 {'extend_existing': True, 'mysql_engine': 'InnoDB',
985 987 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
986 988 )
987 989 __mapper_args__ = {}
988 990
989 991 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
990 992 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
991 993 _email = Column("email", String(255), nullable=True, unique=False, default=None)
992 994 user = relationship('User', lazy='joined')
993 995
994 996 @validates('_email')
995 997 def validate_email(self, key, email):
996 998 # check if this email is not main one
997 999 main_email = Session().query(User).filter(User.email == email).scalar()
998 1000 if main_email is not None:
999 1001 raise AttributeError('email %s is present is user table' % email)
1000 1002 return email
1001 1003
1002 1004 @hybrid_property
1003 1005 def email(self):
1004 1006 return self._email
1005 1007
1006 1008 @email.setter
1007 1009 def email(self, val):
1008 1010 self._email = val.lower() if val else None
1009 1011
1010 1012
1011 1013 class UserIpMap(Base, BaseModel):
1012 1014 __tablename__ = 'user_ip_map'
1013 1015 __table_args__ = (
1014 1016 UniqueConstraint('user_id', 'ip_addr'),
1015 1017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1016 1018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1017 1019 )
1018 1020 __mapper_args__ = {}
1019 1021
1020 1022 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1021 1023 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1022 1024 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1023 1025 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1024 1026 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1025 1027 user = relationship('User', lazy='joined')
1026 1028
1027 1029 @classmethod
1028 1030 def _get_ip_range(cls, ip_addr):
1029 1031 net = ipaddress.ip_network(ip_addr, strict=False)
1030 1032 return [str(net.network_address), str(net.broadcast_address)]
1031 1033
1032 1034 def __json__(self):
1033 1035 return {
1034 1036 'ip_addr': self.ip_addr,
1035 1037 'ip_range': self._get_ip_range(self.ip_addr),
1036 1038 }
1037 1039
1038 1040 def __unicode__(self):
1039 1041 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1040 1042 self.user_id, self.ip_addr)
1041 1043
1042 1044 class UserLog(Base, BaseModel):
1043 1045 __tablename__ = 'user_logs'
1044 1046 __table_args__ = (
1045 1047 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1046 1048 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1047 1049 )
1048 1050 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 1051 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 1052 username = Column("username", String(255), nullable=True, unique=None, default=None)
1051 1053 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1052 1054 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1053 1055 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1054 1056 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1055 1057 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1056 1058
1057 1059 def __unicode__(self):
1058 1060 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1059 1061 self.repository_name,
1060 1062 self.action)
1061 1063
1062 1064 @property
1063 1065 def action_as_day(self):
1064 1066 return datetime.date(*self.action_date.timetuple()[:3])
1065 1067
1066 1068 user = relationship('User')
1067 1069 repository = relationship('Repository', cascade='')
1068 1070
1069 1071
1070 1072 class UserGroup(Base, BaseModel):
1071 1073 __tablename__ = 'users_groups'
1072 1074 __table_args__ = (
1073 1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1074 1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1075 1077 )
1076 1078
1077 1079 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1078 1080 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1079 1081 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1080 1082 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1081 1083 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1082 1084 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1083 1085 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1084 1086 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1085 1087
1086 1088 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1087 1089 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1088 1090 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1089 1091 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1090 1092 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1091 1093 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1092 1094
1093 1095 user = relationship('User')
1094 1096
1095 1097 @hybrid_property
1096 1098 def group_data(self):
1097 1099 if not self._group_data:
1098 1100 return {}
1099 1101
1100 1102 try:
1101 1103 return json.loads(self._group_data)
1102 1104 except TypeError:
1103 1105 return {}
1104 1106
1105 1107 @group_data.setter
1106 1108 def group_data(self, val):
1107 1109 try:
1108 1110 self._group_data = json.dumps(val)
1109 1111 except Exception:
1110 1112 log.error(traceback.format_exc())
1111 1113
1112 1114 def __unicode__(self):
1113 1115 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1114 1116 self.users_group_id,
1115 1117 self.users_group_name)
1116 1118
1117 1119 @classmethod
1118 1120 def get_by_group_name(cls, group_name, cache=False,
1119 1121 case_insensitive=False):
1120 1122 if case_insensitive:
1121 1123 q = cls.query().filter(func.lower(cls.users_group_name) ==
1122 1124 func.lower(group_name))
1123 1125
1124 1126 else:
1125 1127 q = cls.query().filter(cls.users_group_name == group_name)
1126 1128 if cache:
1127 1129 q = q.options(FromCache(
1128 1130 "sql_cache_short",
1129 1131 "get_group_%s" % _hash_key(group_name)))
1130 1132 return q.scalar()
1131 1133
1132 1134 @classmethod
1133 1135 def get(cls, user_group_id, cache=False):
1134 1136 user_group = cls.query()
1135 1137 if cache:
1136 1138 user_group = user_group.options(FromCache("sql_cache_short",
1137 1139 "get_users_group_%s" % user_group_id))
1138 1140 return user_group.get(user_group_id)
1139 1141
1140 1142 def permissions(self, with_admins=True, with_owner=True):
1141 1143 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1142 1144 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1143 1145 joinedload(UserUserGroupToPerm.user),
1144 1146 joinedload(UserUserGroupToPerm.permission),)
1145 1147
1146 1148 # get owners and admins and permissions. We do a trick of re-writing
1147 1149 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1148 1150 # has a global reference and changing one object propagates to all
1149 1151 # others. This means if admin is also an owner admin_row that change
1150 1152 # would propagate to both objects
1151 1153 perm_rows = []
1152 1154 for _usr in q.all():
1153 1155 usr = AttributeDict(_usr.user.get_dict())
1154 1156 usr.permission = _usr.permission.permission_name
1155 1157 perm_rows.append(usr)
1156 1158
1157 1159 # filter the perm rows by 'default' first and then sort them by
1158 1160 # admin,write,read,none permissions sorted again alphabetically in
1159 1161 # each group
1160 1162 perm_rows = sorted(perm_rows, key=display_sort)
1161 1163
1162 1164 _admin_perm = 'usergroup.admin'
1163 1165 owner_row = []
1164 1166 if with_owner:
1165 1167 usr = AttributeDict(self.user.get_dict())
1166 1168 usr.owner_row = True
1167 1169 usr.permission = _admin_perm
1168 1170 owner_row.append(usr)
1169 1171
1170 1172 super_admin_rows = []
1171 1173 if with_admins:
1172 1174 for usr in User.get_all_super_admins():
1173 1175 # if this admin is also owner, don't double the record
1174 1176 if usr.user_id == owner_row[0].user_id:
1175 1177 owner_row[0].admin_row = True
1176 1178 else:
1177 1179 usr = AttributeDict(usr.get_dict())
1178 1180 usr.admin_row = True
1179 1181 usr.permission = _admin_perm
1180 1182 super_admin_rows.append(usr)
1181 1183
1182 1184 return super_admin_rows + owner_row + perm_rows
1183 1185
1184 1186 def permission_user_groups(self):
1185 1187 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1186 1188 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1187 1189 joinedload(UserGroupUserGroupToPerm.target_user_group),
1188 1190 joinedload(UserGroupUserGroupToPerm.permission),)
1189 1191
1190 1192 perm_rows = []
1191 1193 for _user_group in q.all():
1192 1194 usr = AttributeDict(_user_group.user_group.get_dict())
1193 1195 usr.permission = _user_group.permission.permission_name
1194 1196 perm_rows.append(usr)
1195 1197
1196 1198 return perm_rows
1197 1199
1198 1200 def _get_default_perms(self, user_group, suffix=''):
1199 1201 from rhodecode.model.permission import PermissionModel
1200 1202 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1201 1203
1202 1204 def get_default_perms(self, suffix=''):
1203 1205 return self._get_default_perms(self, suffix)
1204 1206
1205 1207 def get_api_data(self, with_group_members=True, include_secrets=False):
1206 1208 """
1207 1209 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1208 1210 basically forwarded.
1209 1211
1210 1212 """
1211 1213 user_group = self
1212 1214
1213 1215 data = {
1214 1216 'users_group_id': user_group.users_group_id,
1215 1217 'group_name': user_group.users_group_name,
1216 1218 'group_description': user_group.user_group_description,
1217 1219 'active': user_group.users_group_active,
1218 1220 'owner': user_group.user.username,
1219 1221 }
1220 1222 if with_group_members:
1221 1223 users = []
1222 1224 for user in user_group.members:
1223 1225 user = user.user
1224 1226 users.append(user.get_api_data(include_secrets=include_secrets))
1225 1227 data['users'] = users
1226 1228
1227 1229 return data
1228 1230
1229 1231
1230 1232 class UserGroupMember(Base, BaseModel):
1231 1233 __tablename__ = 'users_groups_members'
1232 1234 __table_args__ = (
1233 1235 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1234 1236 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1235 1237 )
1236 1238
1237 1239 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1238 1240 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1239 1241 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1240 1242
1241 1243 user = relationship('User', lazy='joined')
1242 1244 users_group = relationship('UserGroup')
1243 1245
1244 1246 def __init__(self, gr_id='', u_id=''):
1245 1247 self.users_group_id = gr_id
1246 1248 self.user_id = u_id
1247 1249
1248 1250
1249 1251 class RepositoryField(Base, BaseModel):
1250 1252 __tablename__ = 'repositories_fields'
1251 1253 __table_args__ = (
1252 1254 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1253 1255 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1254 1256 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1255 1257 )
1256 1258 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1257 1259
1258 1260 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1259 1261 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1260 1262 field_key = Column("field_key", String(250))
1261 1263 field_label = Column("field_label", String(1024), nullable=False)
1262 1264 field_value = Column("field_value", String(10000), nullable=False)
1263 1265 field_desc = Column("field_desc", String(1024), nullable=False)
1264 1266 field_type = Column("field_type", String(255), nullable=False, unique=None)
1265 1267 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1266 1268
1267 1269 repository = relationship('Repository')
1268 1270
1269 1271 @property
1270 1272 def field_key_prefixed(self):
1271 1273 return 'ex_%s' % self.field_key
1272 1274
1273 1275 @classmethod
1274 1276 def un_prefix_key(cls, key):
1275 1277 if key.startswith(cls.PREFIX):
1276 1278 return key[len(cls.PREFIX):]
1277 1279 return key
1278 1280
1279 1281 @classmethod
1280 1282 def get_by_key_name(cls, key, repo):
1281 1283 row = cls.query()\
1282 1284 .filter(cls.repository == repo)\
1283 1285 .filter(cls.field_key == key).scalar()
1284 1286 return row
1285 1287
1286 1288
1287 1289 class Repository(Base, BaseModel):
1288 1290 __tablename__ = 'repositories'
1289 1291 __table_args__ = (
1290 1292 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1291 1293 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1292 1294 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1293 1295 )
1294 1296 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1295 1297 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1296 1298
1297 1299 STATE_CREATED = 'repo_state_created'
1298 1300 STATE_PENDING = 'repo_state_pending'
1299 1301 STATE_ERROR = 'repo_state_error'
1300 1302
1301 1303 LOCK_AUTOMATIC = 'lock_auto'
1302 1304 LOCK_API = 'lock_api'
1303 1305 LOCK_WEB = 'lock_web'
1304 1306 LOCK_PULL = 'lock_pull'
1305 1307
1306 1308 NAME_SEP = URL_SEP
1307 1309
1308 1310 repo_id = Column(
1309 1311 "repo_id", Integer(), nullable=False, unique=True, default=None,
1310 1312 primary_key=True)
1311 1313 _repo_name = Column(
1312 1314 "repo_name", Text(), nullable=False, default=None)
1313 1315 _repo_name_hash = Column(
1314 1316 "repo_name_hash", String(255), nullable=False, unique=True)
1315 1317 repo_state = Column("repo_state", String(255), nullable=True)
1316 1318
1317 1319 clone_uri = Column(
1318 1320 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1319 1321 default=None)
1320 1322 repo_type = Column(
1321 1323 "repo_type", String(255), nullable=False, unique=False, default=None)
1322 1324 user_id = Column(
1323 1325 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1324 1326 unique=False, default=None)
1325 1327 private = Column(
1326 1328 "private", Boolean(), nullable=True, unique=None, default=None)
1327 1329 enable_statistics = Column(
1328 1330 "statistics", Boolean(), nullable=True, unique=None, default=True)
1329 1331 enable_downloads = Column(
1330 1332 "downloads", Boolean(), nullable=True, unique=None, default=True)
1331 1333 description = Column(
1332 1334 "description", String(10000), nullable=True, unique=None, default=None)
1333 1335 created_on = Column(
1334 1336 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1335 1337 default=datetime.datetime.now)
1336 1338 updated_on = Column(
1337 1339 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1338 1340 default=datetime.datetime.now)
1339 1341 _landing_revision = Column(
1340 1342 "landing_revision", String(255), nullable=False, unique=False,
1341 1343 default=None)
1342 1344 enable_locking = Column(
1343 1345 "enable_locking", Boolean(), nullable=False, unique=None,
1344 1346 default=False)
1345 1347 _locked = Column(
1346 1348 "locked", String(255), nullable=True, unique=False, default=None)
1347 1349 _changeset_cache = Column(
1348 1350 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1349 1351
1350 1352 fork_id = Column(
1351 1353 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1352 1354 nullable=True, unique=False, default=None)
1353 1355 group_id = Column(
1354 1356 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1355 1357 unique=False, default=None)
1356 1358
1357 1359 user = relationship('User', lazy='joined')
1358 1360 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1359 1361 group = relationship('RepoGroup', lazy='joined')
1360 1362 repo_to_perm = relationship(
1361 1363 'UserRepoToPerm', cascade='all',
1362 1364 order_by='UserRepoToPerm.repo_to_perm_id')
1363 1365 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1364 1366 stats = relationship('Statistics', cascade='all', uselist=False)
1365 1367
1366 1368 followers = relationship(
1367 1369 'UserFollowing',
1368 1370 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1369 1371 cascade='all')
1370 1372 extra_fields = relationship(
1371 1373 'RepositoryField', cascade="all, delete, delete-orphan")
1372 1374 logs = relationship('UserLog')
1373 1375 comments = relationship(
1374 1376 'ChangesetComment', cascade="all, delete, delete-orphan")
1375 1377 pull_requests_source = relationship(
1376 1378 'PullRequest',
1377 1379 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1378 1380 cascade="all, delete, delete-orphan")
1379 1381 pull_requests_target = relationship(
1380 1382 'PullRequest',
1381 1383 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1382 1384 cascade="all, delete, delete-orphan")
1383 1385 ui = relationship('RepoRhodeCodeUi', cascade="all")
1384 1386 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1385 1387 integrations = relationship('Integration',
1386 1388 cascade="all, delete, delete-orphan")
1387 1389
1388 1390 def __unicode__(self):
1389 1391 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1390 1392 safe_unicode(self.repo_name))
1391 1393
1392 1394 @hybrid_property
1393 1395 def landing_rev(self):
1394 1396 # always should return [rev_type, rev]
1395 1397 if self._landing_revision:
1396 1398 _rev_info = self._landing_revision.split(':')
1397 1399 if len(_rev_info) < 2:
1398 1400 _rev_info.insert(0, 'rev')
1399 1401 return [_rev_info[0], _rev_info[1]]
1400 1402 return [None, None]
1401 1403
1402 1404 @landing_rev.setter
1403 1405 def landing_rev(self, val):
1404 1406 if ':' not in val:
1405 1407 raise ValueError('value must be delimited with `:` and consist '
1406 1408 'of <rev_type>:<rev>, got %s instead' % val)
1407 1409 self._landing_revision = val
1408 1410
1409 1411 @hybrid_property
1410 1412 def locked(self):
1411 1413 if self._locked:
1412 1414 user_id, timelocked, reason = self._locked.split(':')
1413 1415 lock_values = int(user_id), timelocked, reason
1414 1416 else:
1415 1417 lock_values = [None, None, None]
1416 1418 return lock_values
1417 1419
1418 1420 @locked.setter
1419 1421 def locked(self, val):
1420 1422 if val and isinstance(val, (list, tuple)):
1421 1423 self._locked = ':'.join(map(str, val))
1422 1424 else:
1423 1425 self._locked = None
1424 1426
1425 1427 @hybrid_property
1426 1428 def changeset_cache(self):
1427 1429 from rhodecode.lib.vcs.backends.base import EmptyCommit
1428 1430 dummy = EmptyCommit().__json__()
1429 1431 if not self._changeset_cache:
1430 1432 return dummy
1431 1433 try:
1432 1434 return json.loads(self._changeset_cache)
1433 1435 except TypeError:
1434 1436 return dummy
1435 1437 except Exception:
1436 1438 log.error(traceback.format_exc())
1437 1439 return dummy
1438 1440
1439 1441 @changeset_cache.setter
1440 1442 def changeset_cache(self, val):
1441 1443 try:
1442 1444 self._changeset_cache = json.dumps(val)
1443 1445 except Exception:
1444 1446 log.error(traceback.format_exc())
1445 1447
1446 1448 @hybrid_property
1447 1449 def repo_name(self):
1448 1450 return self._repo_name
1449 1451
1450 1452 @repo_name.setter
1451 1453 def repo_name(self, value):
1452 1454 self._repo_name = value
1453 1455 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1454 1456
1455 1457 @classmethod
1456 1458 def normalize_repo_name(cls, repo_name):
1457 1459 """
1458 1460 Normalizes os specific repo_name to the format internally stored inside
1459 1461 database using URL_SEP
1460 1462
1461 1463 :param cls:
1462 1464 :param repo_name:
1463 1465 """
1464 1466 return cls.NAME_SEP.join(repo_name.split(os.sep))
1465 1467
1466 1468 @classmethod
1467 1469 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1468 1470 session = Session()
1469 1471 q = session.query(cls).filter(cls.repo_name == repo_name)
1470 1472
1471 1473 if cache:
1472 1474 if identity_cache:
1473 1475 val = cls.identity_cache(session, 'repo_name', repo_name)
1474 1476 if val:
1475 1477 return val
1476 1478 else:
1477 1479 q = q.options(
1478 1480 FromCache("sql_cache_short",
1479 1481 "get_repo_by_name_%s" % _hash_key(repo_name)))
1480 1482
1481 1483 return q.scalar()
1482 1484
1483 1485 @classmethod
1484 1486 def get_by_full_path(cls, repo_full_path):
1485 1487 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1486 1488 repo_name = cls.normalize_repo_name(repo_name)
1487 1489 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1488 1490
1489 1491 @classmethod
1490 1492 def get_repo_forks(cls, repo_id):
1491 1493 return cls.query().filter(Repository.fork_id == repo_id)
1492 1494
1493 1495 @classmethod
1494 1496 def base_path(cls):
1495 1497 """
1496 1498 Returns base path when all repos are stored
1497 1499
1498 1500 :param cls:
1499 1501 """
1500 1502 q = Session().query(RhodeCodeUi)\
1501 1503 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1502 1504 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1503 1505 return q.one().ui_value
1504 1506
1505 1507 @classmethod
1506 1508 def is_valid(cls, repo_name):
1507 1509 """
1508 1510 returns True if given repo name is a valid filesystem repository
1509 1511
1510 1512 :param cls:
1511 1513 :param repo_name:
1512 1514 """
1513 1515 from rhodecode.lib.utils import is_valid_repo
1514 1516
1515 1517 return is_valid_repo(repo_name, cls.base_path())
1516 1518
1517 1519 @classmethod
1518 1520 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1519 1521 case_insensitive=True):
1520 1522 q = Repository.query()
1521 1523
1522 1524 if not isinstance(user_id, Optional):
1523 1525 q = q.filter(Repository.user_id == user_id)
1524 1526
1525 1527 if not isinstance(group_id, Optional):
1526 1528 q = q.filter(Repository.group_id == group_id)
1527 1529
1528 1530 if case_insensitive:
1529 1531 q = q.order_by(func.lower(Repository.repo_name))
1530 1532 else:
1531 1533 q = q.order_by(Repository.repo_name)
1532 1534 return q.all()
1533 1535
1534 1536 @property
1535 1537 def forks(self):
1536 1538 """
1537 1539 Return forks of this repo
1538 1540 """
1539 1541 return Repository.get_repo_forks(self.repo_id)
1540 1542
1541 1543 @property
1542 1544 def parent(self):
1543 1545 """
1544 1546 Returns fork parent
1545 1547 """
1546 1548 return self.fork
1547 1549
1548 1550 @property
1549 1551 def just_name(self):
1550 1552 return self.repo_name.split(self.NAME_SEP)[-1]
1551 1553
1552 1554 @property
1553 1555 def groups_with_parents(self):
1554 1556 groups = []
1555 1557 if self.group is None:
1556 1558 return groups
1557 1559
1558 1560 cur_gr = self.group
1559 1561 groups.insert(0, cur_gr)
1560 1562 while 1:
1561 1563 gr = getattr(cur_gr, 'parent_group', None)
1562 1564 cur_gr = cur_gr.parent_group
1563 1565 if gr is None:
1564 1566 break
1565 1567 groups.insert(0, gr)
1566 1568
1567 1569 return groups
1568 1570
1569 1571 @property
1570 1572 def groups_and_repo(self):
1571 1573 return self.groups_with_parents, self
1572 1574
1573 1575 @LazyProperty
1574 1576 def repo_path(self):
1575 1577 """
1576 1578 Returns base full path for that repository means where it actually
1577 1579 exists on a filesystem
1578 1580 """
1579 1581 q = Session().query(RhodeCodeUi).filter(
1580 1582 RhodeCodeUi.ui_key == self.NAME_SEP)
1581 1583 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1582 1584 return q.one().ui_value
1583 1585
1584 1586 @property
1585 1587 def repo_full_path(self):
1586 1588 p = [self.repo_path]
1587 1589 # we need to split the name by / since this is how we store the
1588 1590 # names in the database, but that eventually needs to be converted
1589 1591 # into a valid system path
1590 1592 p += self.repo_name.split(self.NAME_SEP)
1591 1593 return os.path.join(*map(safe_unicode, p))
1592 1594
1593 1595 @property
1594 1596 def cache_keys(self):
1595 1597 """
1596 1598 Returns associated cache keys for that repo
1597 1599 """
1598 1600 return CacheKey.query()\
1599 1601 .filter(CacheKey.cache_args == self.repo_name)\
1600 1602 .order_by(CacheKey.cache_key)\
1601 1603 .all()
1602 1604
1603 1605 def get_new_name(self, repo_name):
1604 1606 """
1605 1607 returns new full repository name based on assigned group and new new
1606 1608
1607 1609 :param group_name:
1608 1610 """
1609 1611 path_prefix = self.group.full_path_splitted if self.group else []
1610 1612 return self.NAME_SEP.join(path_prefix + [repo_name])
1611 1613
1612 1614 @property
1613 1615 def _config(self):
1614 1616 """
1615 1617 Returns db based config object.
1616 1618 """
1617 1619 from rhodecode.lib.utils import make_db_config
1618 1620 return make_db_config(clear_session=False, repo=self)
1619 1621
1620 1622 def permissions(self, with_admins=True, with_owner=True):
1621 1623 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1622 1624 q = q.options(joinedload(UserRepoToPerm.repository),
1623 1625 joinedload(UserRepoToPerm.user),
1624 1626 joinedload(UserRepoToPerm.permission),)
1625 1627
1626 1628 # get owners and admins and permissions. We do a trick of re-writing
1627 1629 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1628 1630 # has a global reference and changing one object propagates to all
1629 1631 # others. This means if admin is also an owner admin_row that change
1630 1632 # would propagate to both objects
1631 1633 perm_rows = []
1632 1634 for _usr in q.all():
1633 1635 usr = AttributeDict(_usr.user.get_dict())
1634 1636 usr.permission = _usr.permission.permission_name
1635 1637 perm_rows.append(usr)
1636 1638
1637 1639 # filter the perm rows by 'default' first and then sort them by
1638 1640 # admin,write,read,none permissions sorted again alphabetically in
1639 1641 # each group
1640 1642 perm_rows = sorted(perm_rows, key=display_sort)
1641 1643
1642 1644 _admin_perm = 'repository.admin'
1643 1645 owner_row = []
1644 1646 if with_owner:
1645 1647 usr = AttributeDict(self.user.get_dict())
1646 1648 usr.owner_row = True
1647 1649 usr.permission = _admin_perm
1648 1650 owner_row.append(usr)
1649 1651
1650 1652 super_admin_rows = []
1651 1653 if with_admins:
1652 1654 for usr in User.get_all_super_admins():
1653 1655 # if this admin is also owner, don't double the record
1654 1656 if usr.user_id == owner_row[0].user_id:
1655 1657 owner_row[0].admin_row = True
1656 1658 else:
1657 1659 usr = AttributeDict(usr.get_dict())
1658 1660 usr.admin_row = True
1659 1661 usr.permission = _admin_perm
1660 1662 super_admin_rows.append(usr)
1661 1663
1662 1664 return super_admin_rows + owner_row + perm_rows
1663 1665
1664 1666 def permission_user_groups(self):
1665 1667 q = UserGroupRepoToPerm.query().filter(
1666 1668 UserGroupRepoToPerm.repository == self)
1667 1669 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1668 1670 joinedload(UserGroupRepoToPerm.users_group),
1669 1671 joinedload(UserGroupRepoToPerm.permission),)
1670 1672
1671 1673 perm_rows = []
1672 1674 for _user_group in q.all():
1673 1675 usr = AttributeDict(_user_group.users_group.get_dict())
1674 1676 usr.permission = _user_group.permission.permission_name
1675 1677 perm_rows.append(usr)
1676 1678
1677 1679 return perm_rows
1678 1680
1679 1681 def get_api_data(self, include_secrets=False):
1680 1682 """
1681 1683 Common function for generating repo api data
1682 1684
1683 1685 :param include_secrets: See :meth:`User.get_api_data`.
1684 1686
1685 1687 """
1686 1688 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1687 1689 # move this methods on models level.
1688 1690 from rhodecode.model.settings import SettingsModel
1689 1691
1690 1692 repo = self
1691 1693 _user_id, _time, _reason = self.locked
1692 1694
1693 1695 data = {
1694 1696 'repo_id': repo.repo_id,
1695 1697 'repo_name': repo.repo_name,
1696 1698 'repo_type': repo.repo_type,
1697 1699 'clone_uri': repo.clone_uri or '',
1698 1700 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1699 1701 'private': repo.private,
1700 1702 'created_on': repo.created_on,
1701 1703 'description': repo.description,
1702 1704 'landing_rev': repo.landing_rev,
1703 1705 'owner': repo.user.username,
1704 1706 'fork_of': repo.fork.repo_name if repo.fork else None,
1705 1707 'enable_statistics': repo.enable_statistics,
1706 1708 'enable_locking': repo.enable_locking,
1707 1709 'enable_downloads': repo.enable_downloads,
1708 1710 'last_changeset': repo.changeset_cache,
1709 1711 'locked_by': User.get(_user_id).get_api_data(
1710 1712 include_secrets=include_secrets) if _user_id else None,
1711 1713 'locked_date': time_to_datetime(_time) if _time else None,
1712 1714 'lock_reason': _reason if _reason else None,
1713 1715 }
1714 1716
1715 1717 # TODO: mikhail: should be per-repo settings here
1716 1718 rc_config = SettingsModel().get_all_settings()
1717 1719 repository_fields = str2bool(
1718 1720 rc_config.get('rhodecode_repository_fields'))
1719 1721 if repository_fields:
1720 1722 for f in self.extra_fields:
1721 1723 data[f.field_key_prefixed] = f.field_value
1722 1724
1723 1725 return data
1724 1726
1725 1727 @classmethod
1726 1728 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1727 1729 if not lock_time:
1728 1730 lock_time = time.time()
1729 1731 if not lock_reason:
1730 1732 lock_reason = cls.LOCK_AUTOMATIC
1731 1733 repo.locked = [user_id, lock_time, lock_reason]
1732 1734 Session().add(repo)
1733 1735 Session().commit()
1734 1736
1735 1737 @classmethod
1736 1738 def unlock(cls, repo):
1737 1739 repo.locked = None
1738 1740 Session().add(repo)
1739 1741 Session().commit()
1740 1742
1741 1743 @classmethod
1742 1744 def getlock(cls, repo):
1743 1745 return repo.locked
1744 1746
1745 1747 def is_user_lock(self, user_id):
1746 1748 if self.lock[0]:
1747 1749 lock_user_id = safe_int(self.lock[0])
1748 1750 user_id = safe_int(user_id)
1749 1751 # both are ints, and they are equal
1750 1752 return all([lock_user_id, user_id]) and lock_user_id == user_id
1751 1753
1752 1754 return False
1753 1755
1754 1756 def get_locking_state(self, action, user_id, only_when_enabled=True):
1755 1757 """
1756 1758 Checks locking on this repository, if locking is enabled and lock is
1757 1759 present returns a tuple of make_lock, locked, locked_by.
1758 1760 make_lock can have 3 states None (do nothing) True, make lock
1759 1761 False release lock, This value is later propagated to hooks, which
1760 1762 do the locking. Think about this as signals passed to hooks what to do.
1761 1763
1762 1764 """
1763 1765 # TODO: johbo: This is part of the business logic and should be moved
1764 1766 # into the RepositoryModel.
1765 1767
1766 1768 if action not in ('push', 'pull'):
1767 1769 raise ValueError("Invalid action value: %s" % repr(action))
1768 1770
1769 1771 # defines if locked error should be thrown to user
1770 1772 currently_locked = False
1771 1773 # defines if new lock should be made, tri-state
1772 1774 make_lock = None
1773 1775 repo = self
1774 1776 user = User.get(user_id)
1775 1777
1776 1778 lock_info = repo.locked
1777 1779
1778 1780 if repo and (repo.enable_locking or not only_when_enabled):
1779 1781 if action == 'push':
1780 1782 # check if it's already locked !, if it is compare users
1781 1783 locked_by_user_id = lock_info[0]
1782 1784 if user.user_id == locked_by_user_id:
1783 1785 log.debug(
1784 1786 'Got `push` action from user %s, now unlocking', user)
1785 1787 # unlock if we have push from user who locked
1786 1788 make_lock = False
1787 1789 else:
1788 1790 # we're not the same user who locked, ban with
1789 1791 # code defined in settings (default is 423 HTTP Locked) !
1790 1792 log.debug('Repo %s is currently locked by %s', repo, user)
1791 1793 currently_locked = True
1792 1794 elif action == 'pull':
1793 1795 # [0] user [1] date
1794 1796 if lock_info[0] and lock_info[1]:
1795 1797 log.debug('Repo %s is currently locked by %s', repo, user)
1796 1798 currently_locked = True
1797 1799 else:
1798 1800 log.debug('Setting lock on repo %s by %s', repo, user)
1799 1801 make_lock = True
1800 1802
1801 1803 else:
1802 1804 log.debug('Repository %s do not have locking enabled', repo)
1803 1805
1804 1806 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1805 1807 make_lock, currently_locked, lock_info)
1806 1808
1807 1809 from rhodecode.lib.auth import HasRepoPermissionAny
1808 1810 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1809 1811 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1810 1812 # if we don't have at least write permission we cannot make a lock
1811 1813 log.debug('lock state reset back to FALSE due to lack '
1812 1814 'of at least read permission')
1813 1815 make_lock = False
1814 1816
1815 1817 return make_lock, currently_locked, lock_info
1816 1818
1817 1819 @property
1818 1820 def last_db_change(self):
1819 1821 return self.updated_on
1820 1822
1821 1823 @property
1822 1824 def clone_uri_hidden(self):
1823 1825 clone_uri = self.clone_uri
1824 1826 if clone_uri:
1825 1827 import urlobject
1826 1828 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1827 1829 if url_obj.password:
1828 1830 clone_uri = url_obj.with_password('*****')
1829 1831 return clone_uri
1830 1832
1831 1833 def clone_url(self, **override):
1832 1834 qualified_home_url = url('home', qualified=True)
1833 1835
1834 1836 uri_tmpl = None
1835 1837 if 'with_id' in override:
1836 1838 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1837 1839 del override['with_id']
1838 1840
1839 1841 if 'uri_tmpl' in override:
1840 1842 uri_tmpl = override['uri_tmpl']
1841 1843 del override['uri_tmpl']
1842 1844
1843 1845 # we didn't override our tmpl from **overrides
1844 1846 if not uri_tmpl:
1845 1847 uri_tmpl = self.DEFAULT_CLONE_URI
1846 1848 try:
1847 1849 from pylons import tmpl_context as c
1848 1850 uri_tmpl = c.clone_uri_tmpl
1849 1851 except Exception:
1850 1852 # in any case if we call this outside of request context,
1851 1853 # ie, not having tmpl_context set up
1852 1854 pass
1853 1855
1854 1856 return get_clone_url(uri_tmpl=uri_tmpl,
1855 1857 qualifed_home_url=qualified_home_url,
1856 1858 repo_name=self.repo_name,
1857 1859 repo_id=self.repo_id, **override)
1858 1860
1859 1861 def set_state(self, state):
1860 1862 self.repo_state = state
1861 1863 Session().add(self)
1862 1864 #==========================================================================
1863 1865 # SCM PROPERTIES
1864 1866 #==========================================================================
1865 1867
1866 1868 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1867 1869 return get_commit_safe(
1868 1870 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1869 1871
1870 1872 def get_changeset(self, rev=None, pre_load=None):
1871 1873 warnings.warn("Use get_commit", DeprecationWarning)
1872 1874 commit_id = None
1873 1875 commit_idx = None
1874 1876 if isinstance(rev, basestring):
1875 1877 commit_id = rev
1876 1878 else:
1877 1879 commit_idx = rev
1878 1880 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1879 1881 pre_load=pre_load)
1880 1882
1881 1883 def get_landing_commit(self):
1882 1884 """
1883 1885 Returns landing commit, or if that doesn't exist returns the tip
1884 1886 """
1885 1887 _rev_type, _rev = self.landing_rev
1886 1888 commit = self.get_commit(_rev)
1887 1889 if isinstance(commit, EmptyCommit):
1888 1890 return self.get_commit()
1889 1891 return commit
1890 1892
1891 1893 def update_commit_cache(self, cs_cache=None, config=None):
1892 1894 """
1893 1895 Update cache of last changeset for repository, keys should be::
1894 1896
1895 1897 short_id
1896 1898 raw_id
1897 1899 revision
1898 1900 parents
1899 1901 message
1900 1902 date
1901 1903 author
1902 1904
1903 1905 :param cs_cache:
1904 1906 """
1905 1907 from rhodecode.lib.vcs.backends.base import BaseChangeset
1906 1908 if cs_cache is None:
1907 1909 # use no-cache version here
1908 1910 scm_repo = self.scm_instance(cache=False, config=config)
1909 1911 if scm_repo:
1910 1912 cs_cache = scm_repo.get_commit(
1911 1913 pre_load=["author", "date", "message", "parents"])
1912 1914 else:
1913 1915 cs_cache = EmptyCommit()
1914 1916
1915 1917 if isinstance(cs_cache, BaseChangeset):
1916 1918 cs_cache = cs_cache.__json__()
1917 1919
1918 1920 def is_outdated(new_cs_cache):
1919 1921 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1920 1922 new_cs_cache['revision'] != self.changeset_cache['revision']):
1921 1923 return True
1922 1924 return False
1923 1925
1924 1926 # check if we have maybe already latest cached revision
1925 1927 if is_outdated(cs_cache) or not self.changeset_cache:
1926 1928 _default = datetime.datetime.fromtimestamp(0)
1927 1929 last_change = cs_cache.get('date') or _default
1928 1930 log.debug('updated repo %s with new cs cache %s',
1929 1931 self.repo_name, cs_cache)
1930 1932 self.updated_on = last_change
1931 1933 self.changeset_cache = cs_cache
1932 1934 Session().add(self)
1933 1935 Session().commit()
1934 1936 else:
1935 1937 log.debug('Skipping update_commit_cache for repo:`%s` '
1936 1938 'commit already with latest changes', self.repo_name)
1937 1939
1938 1940 @property
1939 1941 def tip(self):
1940 1942 return self.get_commit('tip')
1941 1943
1942 1944 @property
1943 1945 def author(self):
1944 1946 return self.tip.author
1945 1947
1946 1948 @property
1947 1949 def last_change(self):
1948 1950 return self.scm_instance().last_change
1949 1951
1950 1952 def get_comments(self, revisions=None):
1951 1953 """
1952 1954 Returns comments for this repository grouped by revisions
1953 1955
1954 1956 :param revisions: filter query by revisions only
1955 1957 """
1956 1958 cmts = ChangesetComment.query()\
1957 1959 .filter(ChangesetComment.repo == self)
1958 1960 if revisions:
1959 1961 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1960 1962 grouped = collections.defaultdict(list)
1961 1963 for cmt in cmts.all():
1962 1964 grouped[cmt.revision].append(cmt)
1963 1965 return grouped
1964 1966
1965 1967 def statuses(self, revisions=None):
1966 1968 """
1967 1969 Returns statuses for this repository
1968 1970
1969 1971 :param revisions: list of revisions to get statuses for
1970 1972 """
1971 1973 statuses = ChangesetStatus.query()\
1972 1974 .filter(ChangesetStatus.repo == self)\
1973 1975 .filter(ChangesetStatus.version == 0)
1974 1976
1975 1977 if revisions:
1976 1978 # Try doing the filtering in chunks to avoid hitting limits
1977 1979 size = 500
1978 1980 status_results = []
1979 1981 for chunk in xrange(0, len(revisions), size):
1980 1982 status_results += statuses.filter(
1981 1983 ChangesetStatus.revision.in_(
1982 1984 revisions[chunk: chunk+size])
1983 1985 ).all()
1984 1986 else:
1985 1987 status_results = statuses.all()
1986 1988
1987 1989 grouped = {}
1988 1990
1989 1991 # maybe we have open new pullrequest without a status?
1990 1992 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1991 1993 status_lbl = ChangesetStatus.get_status_lbl(stat)
1992 1994 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1993 1995 for rev in pr.revisions:
1994 1996 pr_id = pr.pull_request_id
1995 1997 pr_repo = pr.target_repo.repo_name
1996 1998 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1997 1999
1998 2000 for stat in status_results:
1999 2001 pr_id = pr_repo = None
2000 2002 if stat.pull_request:
2001 2003 pr_id = stat.pull_request.pull_request_id
2002 2004 pr_repo = stat.pull_request.target_repo.repo_name
2003 2005 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2004 2006 pr_id, pr_repo]
2005 2007 return grouped
2006 2008
2007 2009 # ==========================================================================
2008 2010 # SCM CACHE INSTANCE
2009 2011 # ==========================================================================
2010 2012
2011 2013 def scm_instance(self, **kwargs):
2012 2014 import rhodecode
2013 2015
2014 2016 # Passing a config will not hit the cache currently only used
2015 2017 # for repo2dbmapper
2016 2018 config = kwargs.pop('config', None)
2017 2019 cache = kwargs.pop('cache', None)
2018 2020 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2019 2021 # if cache is NOT defined use default global, else we have a full
2020 2022 # control over cache behaviour
2021 2023 if cache is None and full_cache and not config:
2022 2024 return self._get_instance_cached()
2023 2025 return self._get_instance(cache=bool(cache), config=config)
2024 2026
2025 2027 def _get_instance_cached(self):
2026 2028 @cache_region('long_term')
2027 2029 def _get_repo(cache_key):
2028 2030 return self._get_instance()
2029 2031
2030 2032 invalidator_context = CacheKey.repo_context_cache(
2031 2033 _get_repo, self.repo_name, None, thread_scoped=True)
2032 2034
2033 2035 with invalidator_context as context:
2034 2036 context.invalidate()
2035 2037 repo = context.compute()
2036 2038
2037 2039 return repo
2038 2040
2039 2041 def _get_instance(self, cache=True, config=None):
2040 2042 config = config or self._config
2041 2043 custom_wire = {
2042 2044 'cache': cache # controls the vcs.remote cache
2043 2045 }
2044 2046 repo = get_vcs_instance(
2045 2047 repo_path=safe_str(self.repo_full_path),
2046 2048 config=config,
2047 2049 with_wire=custom_wire,
2048 2050 create=False,
2049 2051 _vcs_alias=self.repo_type)
2050 2052
2051 2053 return repo
2052 2054
2053 2055 def __json__(self):
2054 2056 return {'landing_rev': self.landing_rev}
2055 2057
2056 2058 def get_dict(self):
2057 2059
2058 2060 # Since we transformed `repo_name` to a hybrid property, we need to
2059 2061 # keep compatibility with the code which uses `repo_name` field.
2060 2062
2061 2063 result = super(Repository, self).get_dict()
2062 2064 result['repo_name'] = result.pop('_repo_name', None)
2063 2065 return result
2064 2066
2065 2067
2066 2068 class RepoGroup(Base, BaseModel):
2067 2069 __tablename__ = 'groups'
2068 2070 __table_args__ = (
2069 2071 UniqueConstraint('group_name', 'group_parent_id'),
2070 2072 CheckConstraint('group_id != group_parent_id'),
2071 2073 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2072 2074 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2073 2075 )
2074 2076 __mapper_args__ = {'order_by': 'group_name'}
2075 2077
2076 2078 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2077 2079
2078 2080 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2079 2081 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2080 2082 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2081 2083 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2082 2084 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2083 2085 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2084 2086 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2085 2087 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2086 2088
2087 2089 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2088 2090 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2089 2091 parent_group = relationship('RepoGroup', remote_side=group_id)
2090 2092 user = relationship('User')
2091 2093 integrations = relationship('Integration',
2092 2094 cascade="all, delete, delete-orphan")
2093 2095
2094 2096 def __init__(self, group_name='', parent_group=None):
2095 2097 self.group_name = group_name
2096 2098 self.parent_group = parent_group
2097 2099
2098 2100 def __unicode__(self):
2099 2101 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2100 2102 self.group_name)
2101 2103
2102 2104 @classmethod
2103 2105 def _generate_choice(cls, repo_group):
2104 2106 from webhelpers.html import literal as _literal
2105 2107 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2106 2108 return repo_group.group_id, _name(repo_group.full_path_splitted)
2107 2109
2108 2110 @classmethod
2109 2111 def groups_choices(cls, groups=None, show_empty_group=True):
2110 2112 if not groups:
2111 2113 groups = cls.query().all()
2112 2114
2113 2115 repo_groups = []
2114 2116 if show_empty_group:
2115 2117 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2116 2118
2117 2119 repo_groups.extend([cls._generate_choice(x) for x in groups])
2118 2120
2119 2121 repo_groups = sorted(
2120 2122 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2121 2123 return repo_groups
2122 2124
2123 2125 @classmethod
2124 2126 def url_sep(cls):
2125 2127 return URL_SEP
2126 2128
2127 2129 @classmethod
2128 2130 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2129 2131 if case_insensitive:
2130 2132 gr = cls.query().filter(func.lower(cls.group_name)
2131 2133 == func.lower(group_name))
2132 2134 else:
2133 2135 gr = cls.query().filter(cls.group_name == group_name)
2134 2136 if cache:
2135 2137 gr = gr.options(FromCache(
2136 2138 "sql_cache_short",
2137 2139 "get_group_%s" % _hash_key(group_name)))
2138 2140 return gr.scalar()
2139 2141
2140 2142 @classmethod
2141 2143 def get_user_personal_repo_group(cls, user_id):
2142 2144 user = User.get(user_id)
2143 2145 return cls.query()\
2144 2146 .filter(cls.personal == true())\
2145 2147 .filter(cls.user == user).scalar()
2146 2148
2147 2149 @classmethod
2148 2150 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2149 2151 case_insensitive=True):
2150 2152 q = RepoGroup.query()
2151 2153
2152 2154 if not isinstance(user_id, Optional):
2153 2155 q = q.filter(RepoGroup.user_id == user_id)
2154 2156
2155 2157 if not isinstance(group_id, Optional):
2156 2158 q = q.filter(RepoGroup.group_parent_id == group_id)
2157 2159
2158 2160 if case_insensitive:
2159 2161 q = q.order_by(func.lower(RepoGroup.group_name))
2160 2162 else:
2161 2163 q = q.order_by(RepoGroup.group_name)
2162 2164 return q.all()
2163 2165
2164 2166 @property
2165 2167 def parents(self):
2166 2168 parents_recursion_limit = 10
2167 2169 groups = []
2168 2170 if self.parent_group is None:
2169 2171 return groups
2170 2172 cur_gr = self.parent_group
2171 2173 groups.insert(0, cur_gr)
2172 2174 cnt = 0
2173 2175 while 1:
2174 2176 cnt += 1
2175 2177 gr = getattr(cur_gr, 'parent_group', None)
2176 2178 cur_gr = cur_gr.parent_group
2177 2179 if gr is None:
2178 2180 break
2179 2181 if cnt == parents_recursion_limit:
2180 2182 # this will prevent accidental infinit loops
2181 2183 log.error(('more than %s parents found for group %s, stopping '
2182 2184 'recursive parent fetching' % (parents_recursion_limit, self)))
2183 2185 break
2184 2186
2185 2187 groups.insert(0, gr)
2186 2188 return groups
2187 2189
2188 2190 @property
2189 2191 def children(self):
2190 2192 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2191 2193
2192 2194 @property
2193 2195 def name(self):
2194 2196 return self.group_name.split(RepoGroup.url_sep())[-1]
2195 2197
2196 2198 @property
2197 2199 def full_path(self):
2198 2200 return self.group_name
2199 2201
2200 2202 @property
2201 2203 def full_path_splitted(self):
2202 2204 return self.group_name.split(RepoGroup.url_sep())
2203 2205
2204 2206 @property
2205 2207 def repositories(self):
2206 2208 return Repository.query()\
2207 2209 .filter(Repository.group == self)\
2208 2210 .order_by(Repository.repo_name)
2209 2211
2210 2212 @property
2211 2213 def repositories_recursive_count(self):
2212 2214 cnt = self.repositories.count()
2213 2215
2214 2216 def children_count(group):
2215 2217 cnt = 0
2216 2218 for child in group.children:
2217 2219 cnt += child.repositories.count()
2218 2220 cnt += children_count(child)
2219 2221 return cnt
2220 2222
2221 2223 return cnt + children_count(self)
2222 2224
2223 2225 def _recursive_objects(self, include_repos=True):
2224 2226 all_ = []
2225 2227
2226 2228 def _get_members(root_gr):
2227 2229 if include_repos:
2228 2230 for r in root_gr.repositories:
2229 2231 all_.append(r)
2230 2232 childs = root_gr.children.all()
2231 2233 if childs:
2232 2234 for gr in childs:
2233 2235 all_.append(gr)
2234 2236 _get_members(gr)
2235 2237
2236 2238 _get_members(self)
2237 2239 return [self] + all_
2238 2240
2239 2241 def recursive_groups_and_repos(self):
2240 2242 """
2241 2243 Recursive return all groups, with repositories in those groups
2242 2244 """
2243 2245 return self._recursive_objects()
2244 2246
2245 2247 def recursive_groups(self):
2246 2248 """
2247 2249 Returns all children groups for this group including children of children
2248 2250 """
2249 2251 return self._recursive_objects(include_repos=False)
2250 2252
2251 2253 def get_new_name(self, group_name):
2252 2254 """
2253 2255 returns new full group name based on parent and new name
2254 2256
2255 2257 :param group_name:
2256 2258 """
2257 2259 path_prefix = (self.parent_group.full_path_splitted if
2258 2260 self.parent_group else [])
2259 2261 return RepoGroup.url_sep().join(path_prefix + [group_name])
2260 2262
2261 2263 def permissions(self, with_admins=True, with_owner=True):
2262 2264 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2263 2265 q = q.options(joinedload(UserRepoGroupToPerm.group),
2264 2266 joinedload(UserRepoGroupToPerm.user),
2265 2267 joinedload(UserRepoGroupToPerm.permission),)
2266 2268
2267 2269 # get owners and admins and permissions. We do a trick of re-writing
2268 2270 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2269 2271 # has a global reference and changing one object propagates to all
2270 2272 # others. This means if admin is also an owner admin_row that change
2271 2273 # would propagate to both objects
2272 2274 perm_rows = []
2273 2275 for _usr in q.all():
2274 2276 usr = AttributeDict(_usr.user.get_dict())
2275 2277 usr.permission = _usr.permission.permission_name
2276 2278 perm_rows.append(usr)
2277 2279
2278 2280 # filter the perm rows by 'default' first and then sort them by
2279 2281 # admin,write,read,none permissions sorted again alphabetically in
2280 2282 # each group
2281 2283 perm_rows = sorted(perm_rows, key=display_sort)
2282 2284
2283 2285 _admin_perm = 'group.admin'
2284 2286 owner_row = []
2285 2287 if with_owner:
2286 2288 usr = AttributeDict(self.user.get_dict())
2287 2289 usr.owner_row = True
2288 2290 usr.permission = _admin_perm
2289 2291 owner_row.append(usr)
2290 2292
2291 2293 super_admin_rows = []
2292 2294 if with_admins:
2293 2295 for usr in User.get_all_super_admins():
2294 2296 # if this admin is also owner, don't double the record
2295 2297 if usr.user_id == owner_row[0].user_id:
2296 2298 owner_row[0].admin_row = True
2297 2299 else:
2298 2300 usr = AttributeDict(usr.get_dict())
2299 2301 usr.admin_row = True
2300 2302 usr.permission = _admin_perm
2301 2303 super_admin_rows.append(usr)
2302 2304
2303 2305 return super_admin_rows + owner_row + perm_rows
2304 2306
2305 2307 def permission_user_groups(self):
2306 2308 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2307 2309 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2308 2310 joinedload(UserGroupRepoGroupToPerm.users_group),
2309 2311 joinedload(UserGroupRepoGroupToPerm.permission),)
2310 2312
2311 2313 perm_rows = []
2312 2314 for _user_group in q.all():
2313 2315 usr = AttributeDict(_user_group.users_group.get_dict())
2314 2316 usr.permission = _user_group.permission.permission_name
2315 2317 perm_rows.append(usr)
2316 2318
2317 2319 return perm_rows
2318 2320
2319 2321 def get_api_data(self):
2320 2322 """
2321 2323 Common function for generating api data
2322 2324
2323 2325 """
2324 2326 group = self
2325 2327 data = {
2326 2328 'group_id': group.group_id,
2327 2329 'group_name': group.group_name,
2328 2330 'group_description': group.group_description,
2329 2331 'parent_group': group.parent_group.group_name if group.parent_group else None,
2330 2332 'repositories': [x.repo_name for x in group.repositories],
2331 2333 'owner': group.user.username,
2332 2334 }
2333 2335 return data
2334 2336
2335 2337
2336 2338 class Permission(Base, BaseModel):
2337 2339 __tablename__ = 'permissions'
2338 2340 __table_args__ = (
2339 2341 Index('p_perm_name_idx', 'permission_name'),
2340 2342 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2341 2343 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2342 2344 )
2343 2345 PERMS = [
2344 2346 ('hg.admin', _('RhodeCode Super Administrator')),
2345 2347
2346 2348 ('repository.none', _('Repository no access')),
2347 2349 ('repository.read', _('Repository read access')),
2348 2350 ('repository.write', _('Repository write access')),
2349 2351 ('repository.admin', _('Repository admin access')),
2350 2352
2351 2353 ('group.none', _('Repository group no access')),
2352 2354 ('group.read', _('Repository group read access')),
2353 2355 ('group.write', _('Repository group write access')),
2354 2356 ('group.admin', _('Repository group admin access')),
2355 2357
2356 2358 ('usergroup.none', _('User group no access')),
2357 2359 ('usergroup.read', _('User group read access')),
2358 2360 ('usergroup.write', _('User group write access')),
2359 2361 ('usergroup.admin', _('User group admin access')),
2360 2362
2361 2363 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2362 2364 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2363 2365
2364 2366 ('hg.usergroup.create.false', _('User Group creation disabled')),
2365 2367 ('hg.usergroup.create.true', _('User Group creation enabled')),
2366 2368
2367 2369 ('hg.create.none', _('Repository creation disabled')),
2368 2370 ('hg.create.repository', _('Repository creation enabled')),
2369 2371 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2370 2372 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2371 2373
2372 2374 ('hg.fork.none', _('Repository forking disabled')),
2373 2375 ('hg.fork.repository', _('Repository forking enabled')),
2374 2376
2375 2377 ('hg.register.none', _('Registration disabled')),
2376 2378 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2377 2379 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2378 2380
2379 2381 ('hg.password_reset.enabled', _('Password reset enabled')),
2380 2382 ('hg.password_reset.hidden', _('Password reset hidden')),
2381 2383 ('hg.password_reset.disabled', _('Password reset disabled')),
2382 2384
2383 2385 ('hg.extern_activate.manual', _('Manual activation of external account')),
2384 2386 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2385 2387
2386 2388 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2387 2389 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2388 2390 ]
2389 2391
2390 2392 # definition of system default permissions for DEFAULT user
2391 2393 DEFAULT_USER_PERMISSIONS = [
2392 2394 'repository.read',
2393 2395 'group.read',
2394 2396 'usergroup.read',
2395 2397 'hg.create.repository',
2396 2398 'hg.repogroup.create.false',
2397 2399 'hg.usergroup.create.false',
2398 2400 'hg.create.write_on_repogroup.true',
2399 2401 'hg.fork.repository',
2400 2402 'hg.register.manual_activate',
2401 2403 'hg.password_reset.enabled',
2402 2404 'hg.extern_activate.auto',
2403 2405 'hg.inherit_default_perms.true',
2404 2406 ]
2405 2407
2406 2408 # defines which permissions are more important higher the more important
2407 2409 # Weight defines which permissions are more important.
2408 2410 # The higher number the more important.
2409 2411 PERM_WEIGHTS = {
2410 2412 'repository.none': 0,
2411 2413 'repository.read': 1,
2412 2414 'repository.write': 3,
2413 2415 'repository.admin': 4,
2414 2416
2415 2417 'group.none': 0,
2416 2418 'group.read': 1,
2417 2419 'group.write': 3,
2418 2420 'group.admin': 4,
2419 2421
2420 2422 'usergroup.none': 0,
2421 2423 'usergroup.read': 1,
2422 2424 'usergroup.write': 3,
2423 2425 'usergroup.admin': 4,
2424 2426
2425 2427 'hg.repogroup.create.false': 0,
2426 2428 'hg.repogroup.create.true': 1,
2427 2429
2428 2430 'hg.usergroup.create.false': 0,
2429 2431 'hg.usergroup.create.true': 1,
2430 2432
2431 2433 'hg.fork.none': 0,
2432 2434 'hg.fork.repository': 1,
2433 2435 'hg.create.none': 0,
2434 2436 'hg.create.repository': 1
2435 2437 }
2436 2438
2437 2439 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2438 2440 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2439 2441 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2440 2442
2441 2443 def __unicode__(self):
2442 2444 return u"<%s('%s:%s')>" % (
2443 2445 self.__class__.__name__, self.permission_id, self.permission_name
2444 2446 )
2445 2447
2446 2448 @classmethod
2447 2449 def get_by_key(cls, key):
2448 2450 return cls.query().filter(cls.permission_name == key).scalar()
2449 2451
2450 2452 @classmethod
2451 2453 def get_default_repo_perms(cls, user_id, repo_id=None):
2452 2454 q = Session().query(UserRepoToPerm, Repository, Permission)\
2453 2455 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2454 2456 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2455 2457 .filter(UserRepoToPerm.user_id == user_id)
2456 2458 if repo_id:
2457 2459 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2458 2460 return q.all()
2459 2461
2460 2462 @classmethod
2461 2463 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2462 2464 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2463 2465 .join(
2464 2466 Permission,
2465 2467 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2466 2468 .join(
2467 2469 Repository,
2468 2470 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2469 2471 .join(
2470 2472 UserGroup,
2471 2473 UserGroupRepoToPerm.users_group_id ==
2472 2474 UserGroup.users_group_id)\
2473 2475 .join(
2474 2476 UserGroupMember,
2475 2477 UserGroupRepoToPerm.users_group_id ==
2476 2478 UserGroupMember.users_group_id)\
2477 2479 .filter(
2478 2480 UserGroupMember.user_id == user_id,
2479 2481 UserGroup.users_group_active == true())
2480 2482 if repo_id:
2481 2483 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2482 2484 return q.all()
2483 2485
2484 2486 @classmethod
2485 2487 def get_default_group_perms(cls, user_id, repo_group_id=None):
2486 2488 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2487 2489 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2488 2490 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2489 2491 .filter(UserRepoGroupToPerm.user_id == user_id)
2490 2492 if repo_group_id:
2491 2493 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2492 2494 return q.all()
2493 2495
2494 2496 @classmethod
2495 2497 def get_default_group_perms_from_user_group(
2496 2498 cls, user_id, repo_group_id=None):
2497 2499 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2498 2500 .join(
2499 2501 Permission,
2500 2502 UserGroupRepoGroupToPerm.permission_id ==
2501 2503 Permission.permission_id)\
2502 2504 .join(
2503 2505 RepoGroup,
2504 2506 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2505 2507 .join(
2506 2508 UserGroup,
2507 2509 UserGroupRepoGroupToPerm.users_group_id ==
2508 2510 UserGroup.users_group_id)\
2509 2511 .join(
2510 2512 UserGroupMember,
2511 2513 UserGroupRepoGroupToPerm.users_group_id ==
2512 2514 UserGroupMember.users_group_id)\
2513 2515 .filter(
2514 2516 UserGroupMember.user_id == user_id,
2515 2517 UserGroup.users_group_active == true())
2516 2518 if repo_group_id:
2517 2519 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2518 2520 return q.all()
2519 2521
2520 2522 @classmethod
2521 2523 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2522 2524 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2523 2525 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2524 2526 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2525 2527 .filter(UserUserGroupToPerm.user_id == user_id)
2526 2528 if user_group_id:
2527 2529 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2528 2530 return q.all()
2529 2531
2530 2532 @classmethod
2531 2533 def get_default_user_group_perms_from_user_group(
2532 2534 cls, user_id, user_group_id=None):
2533 2535 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2534 2536 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2535 2537 .join(
2536 2538 Permission,
2537 2539 UserGroupUserGroupToPerm.permission_id ==
2538 2540 Permission.permission_id)\
2539 2541 .join(
2540 2542 TargetUserGroup,
2541 2543 UserGroupUserGroupToPerm.target_user_group_id ==
2542 2544 TargetUserGroup.users_group_id)\
2543 2545 .join(
2544 2546 UserGroup,
2545 2547 UserGroupUserGroupToPerm.user_group_id ==
2546 2548 UserGroup.users_group_id)\
2547 2549 .join(
2548 2550 UserGroupMember,
2549 2551 UserGroupUserGroupToPerm.user_group_id ==
2550 2552 UserGroupMember.users_group_id)\
2551 2553 .filter(
2552 2554 UserGroupMember.user_id == user_id,
2553 2555 UserGroup.users_group_active == true())
2554 2556 if user_group_id:
2555 2557 q = q.filter(
2556 2558 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2557 2559
2558 2560 return q.all()
2559 2561
2560 2562
2561 2563 class UserRepoToPerm(Base, BaseModel):
2562 2564 __tablename__ = 'repo_to_perm'
2563 2565 __table_args__ = (
2564 2566 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2565 2567 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2566 2568 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2567 2569 )
2568 2570 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2569 2571 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2570 2572 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2571 2573 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2572 2574
2573 2575 user = relationship('User')
2574 2576 repository = relationship('Repository')
2575 2577 permission = relationship('Permission')
2576 2578
2577 2579 @classmethod
2578 2580 def create(cls, user, repository, permission):
2579 2581 n = cls()
2580 2582 n.user = user
2581 2583 n.repository = repository
2582 2584 n.permission = permission
2583 2585 Session().add(n)
2584 2586 return n
2585 2587
2586 2588 def __unicode__(self):
2587 2589 return u'<%s => %s >' % (self.user, self.repository)
2588 2590
2589 2591
2590 2592 class UserUserGroupToPerm(Base, BaseModel):
2591 2593 __tablename__ = 'user_user_group_to_perm'
2592 2594 __table_args__ = (
2593 2595 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2594 2596 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2595 2597 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2596 2598 )
2597 2599 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2598 2600 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2599 2601 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2600 2602 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2601 2603
2602 2604 user = relationship('User')
2603 2605 user_group = relationship('UserGroup')
2604 2606 permission = relationship('Permission')
2605 2607
2606 2608 @classmethod
2607 2609 def create(cls, user, user_group, permission):
2608 2610 n = cls()
2609 2611 n.user = user
2610 2612 n.user_group = user_group
2611 2613 n.permission = permission
2612 2614 Session().add(n)
2613 2615 return n
2614 2616
2615 2617 def __unicode__(self):
2616 2618 return u'<%s => %s >' % (self.user, self.user_group)
2617 2619
2618 2620
2619 2621 class UserToPerm(Base, BaseModel):
2620 2622 __tablename__ = 'user_to_perm'
2621 2623 __table_args__ = (
2622 2624 UniqueConstraint('user_id', 'permission_id'),
2623 2625 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2626 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2627 )
2626 2628 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2627 2629 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2628 2630 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2631
2630 2632 user = relationship('User')
2631 2633 permission = relationship('Permission', lazy='joined')
2632 2634
2633 2635 def __unicode__(self):
2634 2636 return u'<%s => %s >' % (self.user, self.permission)
2635 2637
2636 2638
2637 2639 class UserGroupRepoToPerm(Base, BaseModel):
2638 2640 __tablename__ = 'users_group_repo_to_perm'
2639 2641 __table_args__ = (
2640 2642 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2641 2643 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2642 2644 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2643 2645 )
2644 2646 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2645 2647 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2646 2648 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2647 2649 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2648 2650
2649 2651 users_group = relationship('UserGroup')
2650 2652 permission = relationship('Permission')
2651 2653 repository = relationship('Repository')
2652 2654
2653 2655 @classmethod
2654 2656 def create(cls, users_group, repository, permission):
2655 2657 n = cls()
2656 2658 n.users_group = users_group
2657 2659 n.repository = repository
2658 2660 n.permission = permission
2659 2661 Session().add(n)
2660 2662 return n
2661 2663
2662 2664 def __unicode__(self):
2663 2665 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2664 2666
2665 2667
2666 2668 class UserGroupUserGroupToPerm(Base, BaseModel):
2667 2669 __tablename__ = 'user_group_user_group_to_perm'
2668 2670 __table_args__ = (
2669 2671 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2670 2672 CheckConstraint('target_user_group_id != user_group_id'),
2671 2673 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2672 2674 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2673 2675 )
2674 2676 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2675 2677 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2676 2678 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2677 2679 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2678 2680
2679 2681 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2680 2682 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2681 2683 permission = relationship('Permission')
2682 2684
2683 2685 @classmethod
2684 2686 def create(cls, target_user_group, user_group, permission):
2685 2687 n = cls()
2686 2688 n.target_user_group = target_user_group
2687 2689 n.user_group = user_group
2688 2690 n.permission = permission
2689 2691 Session().add(n)
2690 2692 return n
2691 2693
2692 2694 def __unicode__(self):
2693 2695 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2694 2696
2695 2697
2696 2698 class UserGroupToPerm(Base, BaseModel):
2697 2699 __tablename__ = 'users_group_to_perm'
2698 2700 __table_args__ = (
2699 2701 UniqueConstraint('users_group_id', 'permission_id',),
2700 2702 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2701 2703 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2702 2704 )
2703 2705 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2704 2706 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2705 2707 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2706 2708
2707 2709 users_group = relationship('UserGroup')
2708 2710 permission = relationship('Permission')
2709 2711
2710 2712
2711 2713 class UserRepoGroupToPerm(Base, BaseModel):
2712 2714 __tablename__ = 'user_repo_group_to_perm'
2713 2715 __table_args__ = (
2714 2716 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2715 2717 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2716 2718 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2717 2719 )
2718 2720
2719 2721 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2720 2722 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2721 2723 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2722 2724 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2723 2725
2724 2726 user = relationship('User')
2725 2727 group = relationship('RepoGroup')
2726 2728 permission = relationship('Permission')
2727 2729
2728 2730 @classmethod
2729 2731 def create(cls, user, repository_group, permission):
2730 2732 n = cls()
2731 2733 n.user = user
2732 2734 n.group = repository_group
2733 2735 n.permission = permission
2734 2736 Session().add(n)
2735 2737 return n
2736 2738
2737 2739
2738 2740 class UserGroupRepoGroupToPerm(Base, BaseModel):
2739 2741 __tablename__ = 'users_group_repo_group_to_perm'
2740 2742 __table_args__ = (
2741 2743 UniqueConstraint('users_group_id', 'group_id'),
2742 2744 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2745 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2746 )
2745 2747
2746 2748 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)
2747 2749 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2748 2750 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2749 2751 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2750 2752
2751 2753 users_group = relationship('UserGroup')
2752 2754 permission = relationship('Permission')
2753 2755 group = relationship('RepoGroup')
2754 2756
2755 2757 @classmethod
2756 2758 def create(cls, user_group, repository_group, permission):
2757 2759 n = cls()
2758 2760 n.users_group = user_group
2759 2761 n.group = repository_group
2760 2762 n.permission = permission
2761 2763 Session().add(n)
2762 2764 return n
2763 2765
2764 2766 def __unicode__(self):
2765 2767 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2766 2768
2767 2769
2768 2770 class Statistics(Base, BaseModel):
2769 2771 __tablename__ = 'statistics'
2770 2772 __table_args__ = (
2771 2773 UniqueConstraint('repository_id'),
2772 2774 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2773 2775 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2774 2776 )
2775 2777 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2776 2778 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2777 2779 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2778 2780 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2779 2781 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2780 2782 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2781 2783
2782 2784 repository = relationship('Repository', single_parent=True)
2783 2785
2784 2786
2785 2787 class UserFollowing(Base, BaseModel):
2786 2788 __tablename__ = 'user_followings'
2787 2789 __table_args__ = (
2788 2790 UniqueConstraint('user_id', 'follows_repository_id'),
2789 2791 UniqueConstraint('user_id', 'follows_user_id'),
2790 2792 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2791 2793 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2792 2794 )
2793 2795
2794 2796 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2795 2797 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2796 2798 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2797 2799 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2798 2800 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2799 2801
2800 2802 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2801 2803
2802 2804 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2803 2805 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2804 2806
2805 2807 @classmethod
2806 2808 def get_repo_followers(cls, repo_id):
2807 2809 return cls.query().filter(cls.follows_repo_id == repo_id)
2808 2810
2809 2811
2810 2812 class CacheKey(Base, BaseModel):
2811 2813 __tablename__ = 'cache_invalidation'
2812 2814 __table_args__ = (
2813 2815 UniqueConstraint('cache_key'),
2814 2816 Index('key_idx', 'cache_key'),
2815 2817 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2816 2818 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2817 2819 )
2818 2820 CACHE_TYPE_ATOM = 'ATOM'
2819 2821 CACHE_TYPE_RSS = 'RSS'
2820 2822 CACHE_TYPE_README = 'README'
2821 2823
2822 2824 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2823 2825 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2824 2826 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2825 2827 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2826 2828
2827 2829 def __init__(self, cache_key, cache_args=''):
2828 2830 self.cache_key = cache_key
2829 2831 self.cache_args = cache_args
2830 2832 self.cache_active = False
2831 2833
2832 2834 def __unicode__(self):
2833 2835 return u"<%s('%s:%s[%s]')>" % (
2834 2836 self.__class__.__name__,
2835 2837 self.cache_id, self.cache_key, self.cache_active)
2836 2838
2837 2839 def _cache_key_partition(self):
2838 2840 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2839 2841 return prefix, repo_name, suffix
2840 2842
2841 2843 def get_prefix(self):
2842 2844 """
2843 2845 Try to extract prefix from existing cache key. The key could consist
2844 2846 of prefix, repo_name, suffix
2845 2847 """
2846 2848 # this returns prefix, repo_name, suffix
2847 2849 return self._cache_key_partition()[0]
2848 2850
2849 2851 def get_suffix(self):
2850 2852 """
2851 2853 get suffix that might have been used in _get_cache_key to
2852 2854 generate self.cache_key. Only used for informational purposes
2853 2855 in repo_edit.mako.
2854 2856 """
2855 2857 # prefix, repo_name, suffix
2856 2858 return self._cache_key_partition()[2]
2857 2859
2858 2860 @classmethod
2859 2861 def delete_all_cache(cls):
2860 2862 """
2861 2863 Delete all cache keys from database.
2862 2864 Should only be run when all instances are down and all entries
2863 2865 thus stale.
2864 2866 """
2865 2867 cls.query().delete()
2866 2868 Session().commit()
2867 2869
2868 2870 @classmethod
2869 2871 def get_cache_key(cls, repo_name, cache_type):
2870 2872 """
2871 2873
2872 2874 Generate a cache key for this process of RhodeCode instance.
2873 2875 Prefix most likely will be process id or maybe explicitly set
2874 2876 instance_id from .ini file.
2875 2877 """
2876 2878 import rhodecode
2877 2879 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2878 2880
2879 2881 repo_as_unicode = safe_unicode(repo_name)
2880 2882 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2881 2883 if cache_type else repo_as_unicode
2882 2884
2883 2885 return u'{}{}'.format(prefix, key)
2884 2886
2885 2887 @classmethod
2886 2888 def set_invalidate(cls, repo_name, delete=False):
2887 2889 """
2888 2890 Mark all caches of a repo as invalid in the database.
2889 2891 """
2890 2892
2891 2893 try:
2892 2894 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2893 2895 if delete:
2894 2896 log.debug('cache objects deleted for repo %s',
2895 2897 safe_str(repo_name))
2896 2898 qry.delete()
2897 2899 else:
2898 2900 log.debug('cache objects marked as invalid for repo %s',
2899 2901 safe_str(repo_name))
2900 2902 qry.update({"cache_active": False})
2901 2903
2902 2904 Session().commit()
2903 2905 except Exception:
2904 2906 log.exception(
2905 2907 'Cache key invalidation failed for repository %s',
2906 2908 safe_str(repo_name))
2907 2909 Session().rollback()
2908 2910
2909 2911 @classmethod
2910 2912 def get_active_cache(cls, cache_key):
2911 2913 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2912 2914 if inv_obj:
2913 2915 return inv_obj
2914 2916 return None
2915 2917
2916 2918 @classmethod
2917 2919 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2918 2920 thread_scoped=False):
2919 2921 """
2920 2922 @cache_region('long_term')
2921 2923 def _heavy_calculation(cache_key):
2922 2924 return 'result'
2923 2925
2924 2926 cache_context = CacheKey.repo_context_cache(
2925 2927 _heavy_calculation, repo_name, cache_type)
2926 2928
2927 2929 with cache_context as context:
2928 2930 context.invalidate()
2929 2931 computed = context.compute()
2930 2932
2931 2933 assert computed == 'result'
2932 2934 """
2933 2935 from rhodecode.lib import caches
2934 2936 return caches.InvalidationContext(
2935 2937 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2936 2938
2937 2939
2938 2940 class ChangesetComment(Base, BaseModel):
2939 2941 __tablename__ = 'changeset_comments'
2940 2942 __table_args__ = (
2941 2943 Index('cc_revision_idx', 'revision'),
2942 2944 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2943 2945 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2944 2946 )
2945 2947
2946 2948 COMMENT_OUTDATED = u'comment_outdated'
2947 2949 COMMENT_TYPE_NOTE = u'note'
2948 2950 COMMENT_TYPE_TODO = u'todo'
2949 2951 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2950 2952
2951 2953 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2952 2954 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2953 2955 revision = Column('revision', String(40), nullable=True)
2954 2956 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2955 2957 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2956 2958 line_no = Column('line_no', Unicode(10), nullable=True)
2957 2959 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2958 2960 f_path = Column('f_path', Unicode(1000), nullable=True)
2959 2961 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2960 2962 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2961 2963 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2962 2964 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2963 2965 renderer = Column('renderer', Unicode(64), nullable=True)
2964 2966 display_state = Column('display_state', Unicode(128), nullable=True)
2965 2967
2966 2968 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2967 2969 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2968 2970 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2969 2971 author = relationship('User', lazy='joined')
2970 2972 repo = relationship('Repository')
2971 2973 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
2972 2974 pull_request = relationship('PullRequest', lazy='joined')
2973 2975 pull_request_version = relationship('PullRequestVersion')
2974 2976
2975 2977 @classmethod
2976 2978 def get_users(cls, revision=None, pull_request_id=None):
2977 2979 """
2978 2980 Returns user associated with this ChangesetComment. ie those
2979 2981 who actually commented
2980 2982
2981 2983 :param cls:
2982 2984 :param revision:
2983 2985 """
2984 2986 q = Session().query(User)\
2985 2987 .join(ChangesetComment.author)
2986 2988 if revision:
2987 2989 q = q.filter(cls.revision == revision)
2988 2990 elif pull_request_id:
2989 2991 q = q.filter(cls.pull_request_id == pull_request_id)
2990 2992 return q.all()
2991 2993
2992 2994 @classmethod
2993 2995 def get_index_from_version(cls, pr_version, versions):
2994 2996 num_versions = [x.pull_request_version_id for x in versions]
2995 2997 try:
2996 2998 return num_versions.index(pr_version) +1
2997 2999 except (IndexError, ValueError):
2998 3000 return
2999 3001
3000 3002 @property
3001 3003 def outdated(self):
3002 3004 return self.display_state == self.COMMENT_OUTDATED
3003 3005
3004 3006 def outdated_at_version(self, version):
3005 3007 """
3006 3008 Checks if comment is outdated for given pull request version
3007 3009 """
3008 3010 return self.outdated and self.pull_request_version_id != version
3009 3011
3010 3012 def older_than_version(self, version):
3011 3013 """
3012 3014 Checks if comment is made from previous version than given
3013 3015 """
3014 3016 if version is None:
3015 3017 return self.pull_request_version_id is not None
3016 3018
3017 3019 return self.pull_request_version_id < version
3018 3020
3019 3021 @property
3020 3022 def resolved(self):
3021 3023 return self.resolved_by[0] if self.resolved_by else None
3022 3024
3023 3025 @property
3024 3026 def is_todo(self):
3025 3027 return self.comment_type == self.COMMENT_TYPE_TODO
3026 3028
3027 3029 def get_index_version(self, versions):
3028 3030 return self.get_index_from_version(
3029 3031 self.pull_request_version_id, versions)
3030 3032
3031 3033 def render(self, mentions=False):
3032 3034 from rhodecode.lib import helpers as h
3033 3035 return h.render(self.text, renderer=self.renderer, mentions=mentions)
3034 3036
3035 3037 def __repr__(self):
3036 3038 if self.comment_id:
3037 3039 return '<DB:Comment #%s>' % self.comment_id
3038 3040 else:
3039 3041 return '<DB:Comment at %#x>' % id(self)
3040 3042
3041 3043
3042 3044 class ChangesetStatus(Base, BaseModel):
3043 3045 __tablename__ = 'changeset_statuses'
3044 3046 __table_args__ = (
3045 3047 Index('cs_revision_idx', 'revision'),
3046 3048 Index('cs_version_idx', 'version'),
3047 3049 UniqueConstraint('repo_id', 'revision', 'version'),
3048 3050 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3049 3051 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3050 3052 )
3051 3053 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3052 3054 STATUS_APPROVED = 'approved'
3053 3055 STATUS_REJECTED = 'rejected'
3054 3056 STATUS_UNDER_REVIEW = 'under_review'
3055 3057
3056 3058 STATUSES = [
3057 3059 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3058 3060 (STATUS_APPROVED, _("Approved")),
3059 3061 (STATUS_REJECTED, _("Rejected")),
3060 3062 (STATUS_UNDER_REVIEW, _("Under Review")),
3061 3063 ]
3062 3064
3063 3065 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3064 3066 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3065 3067 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3066 3068 revision = Column('revision', String(40), nullable=False)
3067 3069 status = Column('status', String(128), nullable=False, default=DEFAULT)
3068 3070 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3069 3071 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3070 3072 version = Column('version', Integer(), nullable=False, default=0)
3071 3073 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3072 3074
3073 3075 author = relationship('User', lazy='joined')
3074 3076 repo = relationship('Repository')
3075 3077 comment = relationship('ChangesetComment', lazy='joined')
3076 3078 pull_request = relationship('PullRequest', lazy='joined')
3077 3079
3078 3080 def __unicode__(self):
3079 3081 return u"<%s('%s[v%s]:%s')>" % (
3080 3082 self.__class__.__name__,
3081 3083 self.status, self.version, self.author
3082 3084 )
3083 3085
3084 3086 @classmethod
3085 3087 def get_status_lbl(cls, value):
3086 3088 return dict(cls.STATUSES).get(value)
3087 3089
3088 3090 @property
3089 3091 def status_lbl(self):
3090 3092 return ChangesetStatus.get_status_lbl(self.status)
3091 3093
3092 3094
3093 3095 class _PullRequestBase(BaseModel):
3094 3096 """
3095 3097 Common attributes of pull request and version entries.
3096 3098 """
3097 3099
3098 3100 # .status values
3099 3101 STATUS_NEW = u'new'
3100 3102 STATUS_OPEN = u'open'
3101 3103 STATUS_CLOSED = u'closed'
3102 3104
3103 3105 title = Column('title', Unicode(255), nullable=True)
3104 3106 description = Column(
3105 3107 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3106 3108 nullable=True)
3107 3109 # new/open/closed status of pull request (not approve/reject/etc)
3108 3110 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3109 3111 created_on = Column(
3110 3112 'created_on', DateTime(timezone=False), nullable=False,
3111 3113 default=datetime.datetime.now)
3112 3114 updated_on = Column(
3113 3115 'updated_on', DateTime(timezone=False), nullable=False,
3114 3116 default=datetime.datetime.now)
3115 3117
3116 3118 @declared_attr
3117 3119 def user_id(cls):
3118 3120 return Column(
3119 3121 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3120 3122 unique=None)
3121 3123
3122 3124 # 500 revisions max
3123 3125 _revisions = Column(
3124 3126 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3125 3127
3126 3128 @declared_attr
3127 3129 def source_repo_id(cls):
3128 3130 # TODO: dan: rename column to source_repo_id
3129 3131 return Column(
3130 3132 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3131 3133 nullable=False)
3132 3134
3133 3135 source_ref = Column('org_ref', Unicode(255), nullable=False)
3134 3136
3135 3137 @declared_attr
3136 3138 def target_repo_id(cls):
3137 3139 # TODO: dan: rename column to target_repo_id
3138 3140 return Column(
3139 3141 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3140 3142 nullable=False)
3141 3143
3142 3144 target_ref = Column('other_ref', Unicode(255), nullable=False)
3143 3145 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3144 3146
3145 3147 # TODO: dan: rename column to last_merge_source_rev
3146 3148 _last_merge_source_rev = Column(
3147 3149 'last_merge_org_rev', String(40), nullable=True)
3148 3150 # TODO: dan: rename column to last_merge_target_rev
3149 3151 _last_merge_target_rev = Column(
3150 3152 'last_merge_other_rev', String(40), nullable=True)
3151 3153 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3152 3154 merge_rev = Column('merge_rev', String(40), nullable=True)
3153 3155
3154 3156 @hybrid_property
3155 3157 def revisions(self):
3156 3158 return self._revisions.split(':') if self._revisions else []
3157 3159
3158 3160 @revisions.setter
3159 3161 def revisions(self, val):
3160 3162 self._revisions = ':'.join(val)
3161 3163
3162 3164 @declared_attr
3163 3165 def author(cls):
3164 3166 return relationship('User', lazy='joined')
3165 3167
3166 3168 @declared_attr
3167 3169 def source_repo(cls):
3168 3170 return relationship(
3169 3171 'Repository',
3170 3172 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3171 3173
3172 3174 @property
3173 3175 def source_ref_parts(self):
3174 3176 return self.unicode_to_reference(self.source_ref)
3175 3177
3176 3178 @declared_attr
3177 3179 def target_repo(cls):
3178 3180 return relationship(
3179 3181 'Repository',
3180 3182 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3181 3183
3182 3184 @property
3183 3185 def target_ref_parts(self):
3184 3186 return self.unicode_to_reference(self.target_ref)
3185 3187
3186 3188 @property
3187 3189 def shadow_merge_ref(self):
3188 3190 return self.unicode_to_reference(self._shadow_merge_ref)
3189 3191
3190 3192 @shadow_merge_ref.setter
3191 3193 def shadow_merge_ref(self, ref):
3192 3194 self._shadow_merge_ref = self.reference_to_unicode(ref)
3193 3195
3194 3196 def unicode_to_reference(self, raw):
3195 3197 """
3196 3198 Convert a unicode (or string) to a reference object.
3197 3199 If unicode evaluates to False it returns None.
3198 3200 """
3199 3201 if raw:
3200 3202 refs = raw.split(':')
3201 3203 return Reference(*refs)
3202 3204 else:
3203 3205 return None
3204 3206
3205 3207 def reference_to_unicode(self, ref):
3206 3208 """
3207 3209 Convert a reference object to unicode.
3208 3210 If reference is None it returns None.
3209 3211 """
3210 3212 if ref:
3211 3213 return u':'.join(ref)
3212 3214 else:
3213 3215 return None
3214 3216
3215 3217 def get_api_data(self):
3216 3218 from rhodecode.model.pull_request import PullRequestModel
3217 3219 pull_request = self
3218 3220 merge_status = PullRequestModel().merge_status(pull_request)
3219 3221
3220 3222 pull_request_url = url(
3221 3223 'pullrequest_show', repo_name=self.target_repo.repo_name,
3222 3224 pull_request_id=self.pull_request_id, qualified=True)
3223 3225
3224 3226 merge_data = {
3225 3227 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3226 3228 'reference': (
3227 3229 pull_request.shadow_merge_ref._asdict()
3228 3230 if pull_request.shadow_merge_ref else None),
3229 3231 }
3230 3232
3231 3233 data = {
3232 3234 'pull_request_id': pull_request.pull_request_id,
3233 3235 'url': pull_request_url,
3234 3236 'title': pull_request.title,
3235 3237 'description': pull_request.description,
3236 3238 'status': pull_request.status,
3237 3239 'created_on': pull_request.created_on,
3238 3240 'updated_on': pull_request.updated_on,
3239 3241 'commit_ids': pull_request.revisions,
3240 3242 'review_status': pull_request.calculated_review_status(),
3241 3243 'mergeable': {
3242 3244 'status': merge_status[0],
3243 3245 'message': unicode(merge_status[1]),
3244 3246 },
3245 3247 'source': {
3246 3248 'clone_url': pull_request.source_repo.clone_url(),
3247 3249 'repository': pull_request.source_repo.repo_name,
3248 3250 'reference': {
3249 3251 'name': pull_request.source_ref_parts.name,
3250 3252 'type': pull_request.source_ref_parts.type,
3251 3253 'commit_id': pull_request.source_ref_parts.commit_id,
3252 3254 },
3253 3255 },
3254 3256 'target': {
3255 3257 'clone_url': pull_request.target_repo.clone_url(),
3256 3258 'repository': pull_request.target_repo.repo_name,
3257 3259 'reference': {
3258 3260 'name': pull_request.target_ref_parts.name,
3259 3261 'type': pull_request.target_ref_parts.type,
3260 3262 'commit_id': pull_request.target_ref_parts.commit_id,
3261 3263 },
3262 3264 },
3263 3265 'merge': merge_data,
3264 3266 'author': pull_request.author.get_api_data(include_secrets=False,
3265 3267 details='basic'),
3266 3268 'reviewers': [
3267 3269 {
3268 3270 'user': reviewer.get_api_data(include_secrets=False,
3269 3271 details='basic'),
3270 3272 'reasons': reasons,
3271 3273 'review_status': st[0][1].status if st else 'not_reviewed',
3272 3274 }
3273 3275 for reviewer, reasons, st in pull_request.reviewers_statuses()
3274 3276 ]
3275 3277 }
3276 3278
3277 3279 return data
3278 3280
3279 3281
3280 3282 class PullRequest(Base, _PullRequestBase):
3281 3283 __tablename__ = 'pull_requests'
3282 3284 __table_args__ = (
3283 3285 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3284 3286 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3285 3287 )
3286 3288
3287 3289 pull_request_id = Column(
3288 3290 'pull_request_id', Integer(), nullable=False, primary_key=True)
3289 3291
3290 3292 def __repr__(self):
3291 3293 if self.pull_request_id:
3292 3294 return '<DB:PullRequest #%s>' % self.pull_request_id
3293 3295 else:
3294 3296 return '<DB:PullRequest at %#x>' % id(self)
3295 3297
3296 3298 reviewers = relationship('PullRequestReviewers',
3297 3299 cascade="all, delete, delete-orphan")
3298 3300 statuses = relationship('ChangesetStatus')
3299 3301 comments = relationship('ChangesetComment',
3300 3302 cascade="all, delete, delete-orphan")
3301 3303 versions = relationship('PullRequestVersion',
3302 3304 cascade="all, delete, delete-orphan",
3303 3305 lazy='dynamic')
3304 3306
3305 3307 @classmethod
3306 3308 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3307 3309 internal_methods=None):
3308 3310
3309 3311 class PullRequestDisplay(object):
3310 3312 """
3311 3313 Special object wrapper for showing PullRequest data via Versions
3312 3314 It mimics PR object as close as possible. This is read only object
3313 3315 just for display
3314 3316 """
3315 3317
3316 3318 def __init__(self, attrs, internal=None):
3317 3319 self.attrs = attrs
3318 3320 # internal have priority over the given ones via attrs
3319 3321 self.internal = internal or ['versions']
3320 3322
3321 3323 def __getattr__(self, item):
3322 3324 if item in self.internal:
3323 3325 return getattr(self, item)
3324 3326 try:
3325 3327 return self.attrs[item]
3326 3328 except KeyError:
3327 3329 raise AttributeError(
3328 3330 '%s object has no attribute %s' % (self, item))
3329 3331
3330 3332 def __repr__(self):
3331 3333 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3332 3334
3333 3335 def versions(self):
3334 3336 return pull_request_obj.versions.order_by(
3335 3337 PullRequestVersion.pull_request_version_id).all()
3336 3338
3337 3339 def is_closed(self):
3338 3340 return pull_request_obj.is_closed()
3339 3341
3340 3342 @property
3341 3343 def pull_request_version_id(self):
3342 3344 return getattr(pull_request_obj, 'pull_request_version_id', None)
3343 3345
3344 3346 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3345 3347
3346 3348 attrs.author = StrictAttributeDict(
3347 3349 pull_request_obj.author.get_api_data())
3348 3350 if pull_request_obj.target_repo:
3349 3351 attrs.target_repo = StrictAttributeDict(
3350 3352 pull_request_obj.target_repo.get_api_data())
3351 3353 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3352 3354
3353 3355 if pull_request_obj.source_repo:
3354 3356 attrs.source_repo = StrictAttributeDict(
3355 3357 pull_request_obj.source_repo.get_api_data())
3356 3358 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3357 3359
3358 3360 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3359 3361 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3360 3362 attrs.revisions = pull_request_obj.revisions
3361 3363
3362 3364 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3363 3365
3364 3366 return PullRequestDisplay(attrs, internal=internal_methods)
3365 3367
3366 3368 def is_closed(self):
3367 3369 return self.status == self.STATUS_CLOSED
3368 3370
3369 3371 def __json__(self):
3370 3372 return {
3371 3373 'revisions': self.revisions,
3372 3374 }
3373 3375
3374 3376 def calculated_review_status(self):
3375 3377 from rhodecode.model.changeset_status import ChangesetStatusModel
3376 3378 return ChangesetStatusModel().calculated_review_status(self)
3377 3379
3378 3380 def reviewers_statuses(self):
3379 3381 from rhodecode.model.changeset_status import ChangesetStatusModel
3380 3382 return ChangesetStatusModel().reviewers_statuses(self)
3381 3383
3382 3384 @property
3383 3385 def workspace_id(self):
3384 3386 from rhodecode.model.pull_request import PullRequestModel
3385 3387 return PullRequestModel()._workspace_id(self)
3386 3388
3387 3389 def get_shadow_repo(self):
3388 3390 workspace_id = self.workspace_id
3389 3391 vcs_obj = self.target_repo.scm_instance()
3390 3392 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3391 3393 workspace_id)
3392 3394 return vcs_obj._get_shadow_instance(shadow_repository_path)
3393 3395
3394 3396
3395 3397 class PullRequestVersion(Base, _PullRequestBase):
3396 3398 __tablename__ = 'pull_request_versions'
3397 3399 __table_args__ = (
3398 3400 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3399 3401 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3400 3402 )
3401 3403
3402 3404 pull_request_version_id = Column(
3403 3405 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3404 3406 pull_request_id = Column(
3405 3407 'pull_request_id', Integer(),
3406 3408 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3407 3409 pull_request = relationship('PullRequest')
3408 3410
3409 3411 def __repr__(self):
3410 3412 if self.pull_request_version_id:
3411 3413 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3412 3414 else:
3413 3415 return '<DB:PullRequestVersion at %#x>' % id(self)
3414 3416
3415 3417 @property
3416 3418 def reviewers(self):
3417 3419 return self.pull_request.reviewers
3418 3420
3419 3421 @property
3420 3422 def versions(self):
3421 3423 return self.pull_request.versions
3422 3424
3423 3425 def is_closed(self):
3424 3426 # calculate from original
3425 3427 return self.pull_request.status == self.STATUS_CLOSED
3426 3428
3427 3429 def calculated_review_status(self):
3428 3430 return self.pull_request.calculated_review_status()
3429 3431
3430 3432 def reviewers_statuses(self):
3431 3433 return self.pull_request.reviewers_statuses()
3432 3434
3433 3435
3434 3436 class PullRequestReviewers(Base, BaseModel):
3435 3437 __tablename__ = 'pull_request_reviewers'
3436 3438 __table_args__ = (
3437 3439 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3438 3440 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3439 3441 )
3440 3442
3441 3443 def __init__(self, user=None, pull_request=None, reasons=None):
3442 3444 self.user = user
3443 3445 self.pull_request = pull_request
3444 3446 self.reasons = reasons or []
3445 3447
3446 3448 @hybrid_property
3447 3449 def reasons(self):
3448 3450 if not self._reasons:
3449 3451 return []
3450 3452 return self._reasons
3451 3453
3452 3454 @reasons.setter
3453 3455 def reasons(self, val):
3454 3456 val = val or []
3455 3457 if any(not isinstance(x, basestring) for x in val):
3456 3458 raise Exception('invalid reasons type, must be list of strings')
3457 3459 self._reasons = val
3458 3460
3459 3461 pull_requests_reviewers_id = Column(
3460 3462 'pull_requests_reviewers_id', Integer(), nullable=False,
3461 3463 primary_key=True)
3462 3464 pull_request_id = Column(
3463 3465 "pull_request_id", Integer(),
3464 3466 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3465 3467 user_id = Column(
3466 3468 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3467 3469 _reasons = Column(
3468 3470 'reason', MutationList.as_mutable(
3469 3471 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3470 3472
3471 3473 user = relationship('User')
3472 3474 pull_request = relationship('PullRequest')
3473 3475
3474 3476
3475 3477 class Notification(Base, BaseModel):
3476 3478 __tablename__ = 'notifications'
3477 3479 __table_args__ = (
3478 3480 Index('notification_type_idx', 'type'),
3479 3481 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3480 3482 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3481 3483 )
3482 3484
3483 3485 TYPE_CHANGESET_COMMENT = u'cs_comment'
3484 3486 TYPE_MESSAGE = u'message'
3485 3487 TYPE_MENTION = u'mention'
3486 3488 TYPE_REGISTRATION = u'registration'
3487 3489 TYPE_PULL_REQUEST = u'pull_request'
3488 3490 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3489 3491
3490 3492 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3491 3493 subject = Column('subject', Unicode(512), nullable=True)
3492 3494 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3493 3495 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3494 3496 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3495 3497 type_ = Column('type', Unicode(255))
3496 3498
3497 3499 created_by_user = relationship('User')
3498 3500 notifications_to_users = relationship('UserNotification', lazy='joined',
3499 3501 cascade="all, delete, delete-orphan")
3500 3502
3501 3503 @property
3502 3504 def recipients(self):
3503 3505 return [x.user for x in UserNotification.query()\
3504 3506 .filter(UserNotification.notification == self)\
3505 3507 .order_by(UserNotification.user_id.asc()).all()]
3506 3508
3507 3509 @classmethod
3508 3510 def create(cls, created_by, subject, body, recipients, type_=None):
3509 3511 if type_ is None:
3510 3512 type_ = Notification.TYPE_MESSAGE
3511 3513
3512 3514 notification = cls()
3513 3515 notification.created_by_user = created_by
3514 3516 notification.subject = subject
3515 3517 notification.body = body
3516 3518 notification.type_ = type_
3517 3519 notification.created_on = datetime.datetime.now()
3518 3520
3519 3521 for u in recipients:
3520 3522 assoc = UserNotification()
3521 3523 assoc.notification = notification
3522 3524
3523 3525 # if created_by is inside recipients mark his notification
3524 3526 # as read
3525 3527 if u.user_id == created_by.user_id:
3526 3528 assoc.read = True
3527 3529
3528 3530 u.notifications.append(assoc)
3529 3531 Session().add(notification)
3530 3532
3531 3533 return notification
3532 3534
3533 3535 @property
3534 3536 def description(self):
3535 3537 from rhodecode.model.notification import NotificationModel
3536 3538 return NotificationModel().make_description(self)
3537 3539
3538 3540
3539 3541 class UserNotification(Base, BaseModel):
3540 3542 __tablename__ = 'user_to_notification'
3541 3543 __table_args__ = (
3542 3544 UniqueConstraint('user_id', 'notification_id'),
3543 3545 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3544 3546 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3545 3547 )
3546 3548 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3547 3549 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3548 3550 read = Column('read', Boolean, default=False)
3549 3551 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3550 3552
3551 3553 user = relationship('User', lazy="joined")
3552 3554 notification = relationship('Notification', lazy="joined",
3553 3555 order_by=lambda: Notification.created_on.desc(),)
3554 3556
3555 3557 def mark_as_read(self):
3556 3558 self.read = True
3557 3559 Session().add(self)
3558 3560
3559 3561
3560 3562 class Gist(Base, BaseModel):
3561 3563 __tablename__ = 'gists'
3562 3564 __table_args__ = (
3563 3565 Index('g_gist_access_id_idx', 'gist_access_id'),
3564 3566 Index('g_created_on_idx', 'created_on'),
3565 3567 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3566 3568 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3567 3569 )
3568 3570 GIST_PUBLIC = u'public'
3569 3571 GIST_PRIVATE = u'private'
3570 3572 DEFAULT_FILENAME = u'gistfile1.txt'
3571 3573
3572 3574 ACL_LEVEL_PUBLIC = u'acl_public'
3573 3575 ACL_LEVEL_PRIVATE = u'acl_private'
3574 3576
3575 3577 gist_id = Column('gist_id', Integer(), primary_key=True)
3576 3578 gist_access_id = Column('gist_access_id', Unicode(250))
3577 3579 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3578 3580 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3579 3581 gist_expires = Column('gist_expires', Float(53), nullable=False)
3580 3582 gist_type = Column('gist_type', Unicode(128), nullable=False)
3581 3583 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3582 3584 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3583 3585 acl_level = Column('acl_level', Unicode(128), nullable=True)
3584 3586
3585 3587 owner = relationship('User')
3586 3588
3587 3589 def __repr__(self):
3588 3590 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3589 3591
3590 3592 @classmethod
3591 3593 def get_or_404(cls, id_):
3592 3594 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3593 3595 if not res:
3594 3596 raise HTTPNotFound
3595 3597 return res
3596 3598
3597 3599 @classmethod
3598 3600 def get_by_access_id(cls, gist_access_id):
3599 3601 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3600 3602
3601 3603 def gist_url(self):
3602 3604 import rhodecode
3603 3605 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3604 3606 if alias_url:
3605 3607 return alias_url.replace('{gistid}', self.gist_access_id)
3606 3608
3607 3609 return url('gist', gist_id=self.gist_access_id, qualified=True)
3608 3610
3609 3611 @classmethod
3610 3612 def base_path(cls):
3611 3613 """
3612 3614 Returns base path when all gists are stored
3613 3615
3614 3616 :param cls:
3615 3617 """
3616 3618 from rhodecode.model.gist import GIST_STORE_LOC
3617 3619 q = Session().query(RhodeCodeUi)\
3618 3620 .filter(RhodeCodeUi.ui_key == URL_SEP)
3619 3621 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3620 3622 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3621 3623
3622 3624 def get_api_data(self):
3623 3625 """
3624 3626 Common function for generating gist related data for API
3625 3627 """
3626 3628 gist = self
3627 3629 data = {
3628 3630 'gist_id': gist.gist_id,
3629 3631 'type': gist.gist_type,
3630 3632 'access_id': gist.gist_access_id,
3631 3633 'description': gist.gist_description,
3632 3634 'url': gist.gist_url(),
3633 3635 'expires': gist.gist_expires,
3634 3636 'created_on': gist.created_on,
3635 3637 'modified_at': gist.modified_at,
3636 3638 'content': None,
3637 3639 'acl_level': gist.acl_level,
3638 3640 }
3639 3641 return data
3640 3642
3641 3643 def __json__(self):
3642 3644 data = dict(
3643 3645 )
3644 3646 data.update(self.get_api_data())
3645 3647 return data
3646 3648 # SCM functions
3647 3649
3648 3650 def scm_instance(self, **kwargs):
3649 3651 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3650 3652 return get_vcs_instance(
3651 3653 repo_path=safe_str(full_repo_path), create=False)
3652 3654
3653 3655
3654 3656 class ExternalIdentity(Base, BaseModel):
3655 3657 __tablename__ = 'external_identities'
3656 3658 __table_args__ = (
3657 3659 Index('local_user_id_idx', 'local_user_id'),
3658 3660 Index('external_id_idx', 'external_id'),
3659 3661 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3660 3662 'mysql_charset': 'utf8'})
3661 3663
3662 3664 external_id = Column('external_id', Unicode(255), default=u'',
3663 3665 primary_key=True)
3664 3666 external_username = Column('external_username', Unicode(1024), default=u'')
3665 3667 local_user_id = Column('local_user_id', Integer(),
3666 3668 ForeignKey('users.user_id'), primary_key=True)
3667 3669 provider_name = Column('provider_name', Unicode(255), default=u'',
3668 3670 primary_key=True)
3669 3671 access_token = Column('access_token', String(1024), default=u'')
3670 3672 alt_token = Column('alt_token', String(1024), default=u'')
3671 3673 token_secret = Column('token_secret', String(1024), default=u'')
3672 3674
3673 3675 @classmethod
3674 3676 def by_external_id_and_provider(cls, external_id, provider_name,
3675 3677 local_user_id=None):
3676 3678 """
3677 3679 Returns ExternalIdentity instance based on search params
3678 3680
3679 3681 :param external_id:
3680 3682 :param provider_name:
3681 3683 :return: ExternalIdentity
3682 3684 """
3683 3685 query = cls.query()
3684 3686 query = query.filter(cls.external_id == external_id)
3685 3687 query = query.filter(cls.provider_name == provider_name)
3686 3688 if local_user_id:
3687 3689 query = query.filter(cls.local_user_id == local_user_id)
3688 3690 return query.first()
3689 3691
3690 3692 @classmethod
3691 3693 def user_by_external_id_and_provider(cls, external_id, provider_name):
3692 3694 """
3693 3695 Returns User instance based on search params
3694 3696
3695 3697 :param external_id:
3696 3698 :param provider_name:
3697 3699 :return: User
3698 3700 """
3699 3701 query = User.query()
3700 3702 query = query.filter(cls.external_id == external_id)
3701 3703 query = query.filter(cls.provider_name == provider_name)
3702 3704 query = query.filter(User.user_id == cls.local_user_id)
3703 3705 return query.first()
3704 3706
3705 3707 @classmethod
3706 3708 def by_local_user_id(cls, local_user_id):
3707 3709 """
3708 3710 Returns all tokens for user
3709 3711
3710 3712 :param local_user_id:
3711 3713 :return: ExternalIdentity
3712 3714 """
3713 3715 query = cls.query()
3714 3716 query = query.filter(cls.local_user_id == local_user_id)
3715 3717 return query
3716 3718
3717 3719
3718 3720 class Integration(Base, BaseModel):
3719 3721 __tablename__ = 'integrations'
3720 3722 __table_args__ = (
3721 3723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3722 3724 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3723 3725 )
3724 3726
3725 3727 integration_id = Column('integration_id', Integer(), primary_key=True)
3726 3728 integration_type = Column('integration_type', String(255))
3727 3729 enabled = Column('enabled', Boolean(), nullable=False)
3728 3730 name = Column('name', String(255), nullable=False)
3729 3731 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3730 3732 default=False)
3731 3733
3732 3734 settings = Column(
3733 3735 'settings_json', MutationObj.as_mutable(
3734 3736 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3735 3737 repo_id = Column(
3736 3738 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3737 3739 nullable=True, unique=None, default=None)
3738 3740 repo = relationship('Repository', lazy='joined')
3739 3741
3740 3742 repo_group_id = Column(
3741 3743 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3742 3744 nullable=True, unique=None, default=None)
3743 3745 repo_group = relationship('RepoGroup', lazy='joined')
3744 3746
3745 3747 @property
3746 3748 def scope(self):
3747 3749 if self.repo:
3748 3750 return repr(self.repo)
3749 3751 if self.repo_group:
3750 3752 if self.child_repos_only:
3751 3753 return repr(self.repo_group) + ' (child repos only)'
3752 3754 else:
3753 3755 return repr(self.repo_group) + ' (recursive)'
3754 3756 if self.child_repos_only:
3755 3757 return 'root_repos'
3756 3758 return 'global'
3757 3759
3758 3760 def __repr__(self):
3759 3761 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3760 3762
3761 3763
3762 3764 class RepoReviewRuleUser(Base, BaseModel):
3763 3765 __tablename__ = 'repo_review_rules_users'
3764 3766 __table_args__ = (
3765 3767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3766 3768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3767 3769 )
3768 3770 repo_review_rule_user_id = Column(
3769 3771 'repo_review_rule_user_id', Integer(), primary_key=True)
3770 3772 repo_review_rule_id = Column("repo_review_rule_id",
3771 3773 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3772 3774 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3773 3775 nullable=False)
3774 3776 user = relationship('User')
3775 3777
3776 3778
3777 3779 class RepoReviewRuleUserGroup(Base, BaseModel):
3778 3780 __tablename__ = 'repo_review_rules_users_groups'
3779 3781 __table_args__ = (
3780 3782 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3781 3783 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3782 3784 )
3783 3785 repo_review_rule_users_group_id = Column(
3784 3786 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3785 3787 repo_review_rule_id = Column("repo_review_rule_id",
3786 3788 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3787 3789 users_group_id = Column("users_group_id", Integer(),
3788 3790 ForeignKey('users_groups.users_group_id'), nullable=False)
3789 3791 users_group = relationship('UserGroup')
3790 3792
3791 3793
3792 3794 class RepoReviewRule(Base, BaseModel):
3793 3795 __tablename__ = 'repo_review_rules'
3794 3796 __table_args__ = (
3795 3797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3796 3798 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3797 3799 )
3798 3800
3799 3801 repo_review_rule_id = Column(
3800 3802 'repo_review_rule_id', Integer(), primary_key=True)
3801 3803 repo_id = Column(
3802 3804 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3803 3805 repo = relationship('Repository', backref='review_rules')
3804 3806
3805 3807 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3806 3808 default=u'*') # glob
3807 3809 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3808 3810 default=u'*') # glob
3809 3811
3810 3812 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3811 3813 nullable=False, default=False)
3812 3814 rule_users = relationship('RepoReviewRuleUser')
3813 3815 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3814 3816
3815 3817 @hybrid_property
3816 3818 def branch_pattern(self):
3817 3819 return self._branch_pattern or '*'
3818 3820
3819 3821 def _validate_glob(self, value):
3820 3822 re.compile('^' + glob2re(value) + '$')
3821 3823
3822 3824 @branch_pattern.setter
3823 3825 def branch_pattern(self, value):
3824 3826 self._validate_glob(value)
3825 3827 self._branch_pattern = value or '*'
3826 3828
3827 3829 @hybrid_property
3828 3830 def file_pattern(self):
3829 3831 return self._file_pattern or '*'
3830 3832
3831 3833 @file_pattern.setter
3832 3834 def file_pattern(self, value):
3833 3835 self._validate_glob(value)
3834 3836 self._file_pattern = value or '*'
3835 3837
3836 3838 def matches(self, branch, files_changed):
3837 3839 """
3838 3840 Check if this review rule matches a branch/files in a pull request
3839 3841
3840 3842 :param branch: branch name for the commit
3841 3843 :param files_changed: list of file paths changed in the pull request
3842 3844 """
3843 3845
3844 3846 branch = branch or ''
3845 3847 files_changed = files_changed or []
3846 3848
3847 3849 branch_matches = True
3848 3850 if branch:
3849 3851 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3850 3852 branch_matches = bool(branch_regex.search(branch))
3851 3853
3852 3854 files_matches = True
3853 3855 if self.file_pattern != '*':
3854 3856 files_matches = False
3855 3857 file_regex = re.compile(glob2re(self.file_pattern))
3856 3858 for filename in files_changed:
3857 3859 if file_regex.search(filename):
3858 3860 files_matches = True
3859 3861 break
3860 3862
3861 3863 return branch_matches and files_matches
3862 3864
3863 3865 @property
3864 3866 def review_users(self):
3865 3867 """ Returns the users which this rule applies to """
3866 3868
3867 3869 users = set()
3868 3870 users |= set([
3869 3871 rule_user.user for rule_user in self.rule_users
3870 3872 if rule_user.user.active])
3871 3873 users |= set(
3872 3874 member.user
3873 3875 for rule_user_group in self.rule_user_groups
3874 3876 for member in rule_user_group.users_group.members
3875 3877 if member.user.active
3876 3878 )
3877 3879 return users
3878 3880
3879 3881 def __repr__(self):
3880 3882 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3881 3883 self.repo_review_rule_id, self.repo)
3882 3884
3883 3885
3884 3886 class DbMigrateVersion(Base, BaseModel):
3885 3887 __tablename__ = 'db_migrate_version'
3886 3888 __table_args__ = (
3887 3889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3888 3890 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3889 3891 )
3890 3892 repository_id = Column('repository_id', String(250), primary_key=True)
3891 3893 repository_path = Column('repository_path', Text)
3892 3894 version = Column('version', Integer)
3893 3895
3894 3896
3895 3897 class DbSession(Base, BaseModel):
3896 3898 __tablename__ = 'db_session'
3897 3899 __table_args__ = (
3898 3900 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3899 3901 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3900 3902 )
3901 3903
3902 3904 def __repr__(self):
3903 3905 return '<DB:DbSession({})>'.format(self.id)
3904 3906
3905 3907 id = Column('id', Integer())
3906 3908 namespace = Column('namespace', String(255), primary_key=True)
3907 3909 accessed = Column('accessed', DateTime, nullable=False)
3908 3910 created = Column('created', DateTime, nullable=False)
3909 3911 data = Column('data', PickleType, nullable=False)
@@ -1,845 +1,852 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode import events
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict, str2bool)
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.db import (
43 43 User, UserToPerm, UserEmailMap, UserIpMap)
44 44 from rhodecode.lib.exceptions import (
45 45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(FromCache("sql_cache_short",
61 61 "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 80 return User.get_by_email(email, case_insensitive, cache)
81 81
82 82 def get_by_auth_token(self, auth_token, cache=False):
83 83 return User.get_by_auth_token(auth_token, cache)
84 84
85 85 def get_active_user_count(self, cache=False):
86 86 return User.query().filter(
87 87 User.active == True).filter(
88 88 User.username != User.DEFAULT_USER).count()
89 89
90 90 def create(self, form_data, cur_user=None):
91 91 if not cur_user:
92 92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 93
94 94 user_data = {
95 95 'username': form_data['username'],
96 96 'password': form_data['password'],
97 97 'email': form_data['email'],
98 98 'firstname': form_data['firstname'],
99 99 'lastname': form_data['lastname'],
100 100 'active': form_data['active'],
101 101 'extern_type': form_data['extern_type'],
102 102 'extern_name': form_data['extern_name'],
103 103 'admin': False,
104 104 'cur_user': cur_user
105 105 }
106 106
107 107 if 'create_repo_group' in form_data:
108 108 user_data['create_repo_group'] = str2bool(
109 109 form_data.get('create_repo_group'))
110 110
111 111 try:
112 112 if form_data.get('password_change'):
113 113 user_data['force_password_change'] = True
114 114 return UserModel().create_or_update(**user_data)
115 115 except Exception:
116 116 log.error(traceback.format_exc())
117 117 raise
118 118
119 119 def update_user(self, user, skip_attrs=None, **kwargs):
120 120 from rhodecode.lib.auth import get_crypt_password
121 121
122 122 user = self._get_user(user)
123 123 if user.username == User.DEFAULT_USER:
124 124 raise DefaultUserException(
125 125 _("You can't Edit this user since it's"
126 126 " crucial for entire application"))
127 127
128 128 # first store only defaults
129 129 user_attrs = {
130 130 'updating_user_id': user.user_id,
131 131 'username': user.username,
132 132 'password': user.password,
133 133 'email': user.email,
134 134 'firstname': user.name,
135 135 'lastname': user.lastname,
136 136 'active': user.active,
137 137 'admin': user.admin,
138 138 'extern_name': user.extern_name,
139 139 'extern_type': user.extern_type,
140 140 'language': user.user_data.get('language')
141 141 }
142 142
143 143 # in case there's new_password, that comes from form, use it to
144 144 # store password
145 145 if kwargs.get('new_password'):
146 146 kwargs['password'] = kwargs['new_password']
147 147
148 148 # cleanups, my_account password change form
149 149 kwargs.pop('current_password', None)
150 150 kwargs.pop('new_password', None)
151 151
152 152 # cleanups, user edit password change form
153 153 kwargs.pop('password_confirmation', None)
154 154 kwargs.pop('password_change', None)
155 155
156 156 # create repo group on user creation
157 157 kwargs.pop('create_repo_group', None)
158 158
159 159 # legacy forms send name, which is the firstname
160 160 firstname = kwargs.pop('name', None)
161 161 if firstname:
162 162 kwargs['firstname'] = firstname
163 163
164 164 for k, v in kwargs.items():
165 165 # skip if we don't want to update this
166 166 if skip_attrs and k in skip_attrs:
167 167 continue
168 168
169 169 user_attrs[k] = v
170 170
171 171 try:
172 172 return self.create_or_update(**user_attrs)
173 173 except Exception:
174 174 log.error(traceback.format_exc())
175 175 raise
176 176
177 177 def create_or_update(
178 178 self, username, password, email, firstname='', lastname='',
179 179 active=True, admin=False, extern_type=None, extern_name=None,
180 180 cur_user=None, plugin=None, force_password_change=False,
181 181 allow_to_create_user=True, create_repo_group=None,
182 182 updating_user_id=None, language=None, strict_creation_check=True):
183 183 """
184 184 Creates a new instance if not found, or updates current one
185 185
186 186 :param username:
187 187 :param password:
188 188 :param email:
189 189 :param firstname:
190 190 :param lastname:
191 191 :param active:
192 192 :param admin:
193 193 :param extern_type:
194 194 :param extern_name:
195 195 :param cur_user:
196 196 :param plugin: optional plugin this method was called from
197 197 :param force_password_change: toggles new or existing user flag
198 198 for password change
199 199 :param allow_to_create_user: Defines if the method can actually create
200 200 new users
201 201 :param create_repo_group: Defines if the method should also
202 202 create an repo group with user name, and owner
203 203 :param updating_user_id: if we set it up this is the user we want to
204 204 update this allows to editing username.
205 205 :param language: language of user from interface.
206 206
207 207 :returns: new User object with injected `is_new_user` attribute.
208 208 """
209 209 if not cur_user:
210 210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211 211
212 212 from rhodecode.lib.auth import (
213 213 get_crypt_password, check_password, generate_auth_token)
214 214 from rhodecode.lib.hooks_base import (
215 215 log_create_user, check_allowed_create_user)
216 216
217 217 def _password_change(new_user, password):
218 218 # empty password
219 219 if not new_user.password:
220 220 return False
221 221
222 222 # password check is only needed for RhodeCode internal auth calls
223 223 # in case it's a plugin we don't care
224 224 if not plugin:
225 225
226 226 # first check if we gave crypted password back, and if it
227 227 # matches it's not password change
228 228 if new_user.password == password:
229 229 return False
230 230
231 231 password_match = check_password(password, new_user.password)
232 232 if not password_match:
233 233 return True
234 234
235 235 return False
236 236
237 237 # read settings on default personal repo group creation
238 238 if create_repo_group is None:
239 239 default_create_repo_group = RepoGroupModel()\
240 240 .get_default_create_personal_repo_group()
241 241 create_repo_group = default_create_repo_group
242 242
243 243 user_data = {
244 244 'username': username,
245 245 'password': password,
246 246 'email': email,
247 247 'firstname': firstname,
248 248 'lastname': lastname,
249 249 'active': active,
250 250 'admin': admin
251 251 }
252 252
253 253 if updating_user_id:
254 254 log.debug('Checking for existing account in RhodeCode '
255 255 'database with user_id `%s` ' % (updating_user_id,))
256 256 user = User.get(updating_user_id)
257 257 else:
258 258 log.debug('Checking for existing account in RhodeCode '
259 259 'database with username `%s` ' % (username,))
260 260 user = User.get_by_username(username, case_insensitive=True)
261 261
262 262 if user is None:
263 263 # we check internal flag if this method is actually allowed to
264 264 # create new user
265 265 if not allow_to_create_user:
266 266 msg = ('Method wants to create new user, but it is not '
267 267 'allowed to do so')
268 268 log.warning(msg)
269 269 raise NotAllowedToCreateUserError(msg)
270 270
271 271 log.debug('Creating new user %s', username)
272 272
273 273 # only if we create user that is active
274 274 new_active_user = active
275 275 if new_active_user and strict_creation_check:
276 276 # raises UserCreationError if it's not allowed for any reason to
277 277 # create new active user, this also executes pre-create hooks
278 278 check_allowed_create_user(user_data, cur_user, strict_check=True)
279 279 events.trigger(events.UserPreCreate(user_data))
280 280 new_user = User()
281 281 edit = False
282 282 else:
283 283 log.debug('updating user %s', username)
284 284 events.trigger(events.UserPreUpdate(user, user_data))
285 285 new_user = user
286 286 edit = True
287 287
288 288 # we're not allowed to edit default user
289 289 if user.username == User.DEFAULT_USER:
290 290 raise DefaultUserException(
291 291 _("You can't edit this user (`%(username)s`) since it's "
292 292 "crucial for entire application") % {'username': user.username})
293 293
294 294 # inject special attribute that will tell us if User is new or old
295 295 new_user.is_new_user = not edit
296 296 # for users that didn's specify auth type, we use RhodeCode built in
297 297 from rhodecode.authentication.plugins import auth_rhodecode
298 298 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
299 299 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
300 300
301 301 try:
302 302 new_user.username = username
303 303 new_user.admin = admin
304 304 new_user.email = email
305 305 new_user.active = active
306 306 new_user.extern_name = safe_unicode(extern_name)
307 307 new_user.extern_type = safe_unicode(extern_type)
308 308 new_user.name = firstname
309 309 new_user.lastname = lastname
310 310
311 311 if not edit:
312 312 new_user.api_key = generate_auth_token(username)
313 313
314 314 # set password only if creating an user or password is changed
315 315 if not edit or _password_change(new_user, password):
316 316 reason = 'new password' if edit else 'new user'
317 317 log.debug('Updating password reason=>%s', reason)
318 318 new_user.password = get_crypt_password(password) if password else None
319 319
320 320 if force_password_change:
321 321 new_user.update_userdata(force_password_change=True)
322 322 if language:
323 323 new_user.update_userdata(language=language)
324 324 new_user.update_userdata(notification_status=True)
325 325
326 326 self.sa.add(new_user)
327 327
328 328 if not edit and create_repo_group:
329 329 RepoGroupModel().create_personal_repo_group(
330 330 new_user, commit_early=False)
331 331
332 332 if not edit:
333 333 # add the RSS token
334 334 AuthTokenModel().create(username,
335 335 description='Generated feed token',
336 336 role=AuthTokenModel.cls.ROLE_FEED)
337 337 log_create_user(created_by=cur_user, **new_user.get_dict())
338 338 events.trigger(events.UserPostCreate(user_data))
339 339 return new_user
340 340 except (DatabaseError,):
341 341 log.error(traceback.format_exc())
342 342 raise
343 343
344 344 def create_registration(self, form_data):
345 345 from rhodecode.model.notification import NotificationModel
346 346 from rhodecode.model.notification import EmailNotificationModel
347 347
348 348 try:
349 349 form_data['admin'] = False
350 350 form_data['extern_name'] = 'rhodecode'
351 351 form_data['extern_type'] = 'rhodecode'
352 352 new_user = self.create(form_data)
353 353
354 354 self.sa.add(new_user)
355 355 self.sa.flush()
356 356
357 357 user_data = new_user.get_dict()
358 358 kwargs = {
359 359 # use SQLALCHEMY safe dump of user data
360 360 'user': AttributeDict(user_data),
361 361 'date': datetime.datetime.now()
362 362 }
363 363 notification_type = EmailNotificationModel.TYPE_REGISTRATION
364 364 # pre-generate the subject for notification itself
365 365 (subject,
366 366 _h, _e, # we don't care about those
367 367 body_plaintext) = EmailNotificationModel().render_email(
368 368 notification_type, **kwargs)
369 369
370 370 # create notification objects, and emails
371 371 NotificationModel().create(
372 372 created_by=new_user,
373 373 notification_subject=subject,
374 374 notification_body=body_plaintext,
375 375 notification_type=notification_type,
376 376 recipients=None, # all admins
377 377 email_kwargs=kwargs,
378 378 )
379 379
380 380 return new_user
381 381 except Exception:
382 382 log.error(traceback.format_exc())
383 383 raise
384 384
385 385 def _handle_user_repos(self, username, repositories, handle_mode=None):
386 386 _superadmin = self.cls.get_first_super_admin()
387 387 left_overs = True
388 388
389 389 from rhodecode.model.repo import RepoModel
390 390
391 391 if handle_mode == 'detach':
392 392 for obj in repositories:
393 393 obj.user = _superadmin
394 394 # set description we know why we super admin now owns
395 395 # additional repositories that were orphaned !
396 396 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
397 397 self.sa.add(obj)
398 398 left_overs = False
399 399 elif handle_mode == 'delete':
400 400 for obj in repositories:
401 401 RepoModel().delete(obj, forks='detach')
402 402 left_overs = False
403 403
404 404 # if nothing is done we have left overs left
405 405 return left_overs
406 406
407 407 def _handle_user_repo_groups(self, username, repository_groups,
408 408 handle_mode=None):
409 409 _superadmin = self.cls.get_first_super_admin()
410 410 left_overs = True
411 411
412 412 from rhodecode.model.repo_group import RepoGroupModel
413 413
414 414 if handle_mode == 'detach':
415 415 for r in repository_groups:
416 416 r.user = _superadmin
417 417 # set description we know why we super admin now owns
418 418 # additional repositories that were orphaned !
419 419 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
420 420 self.sa.add(r)
421 421 left_overs = False
422 422 elif handle_mode == 'delete':
423 423 for r in repository_groups:
424 424 RepoGroupModel().delete(r)
425 425 left_overs = False
426 426
427 427 # if nothing is done we have left overs left
428 428 return left_overs
429 429
430 430 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
431 431 _superadmin = self.cls.get_first_super_admin()
432 432 left_overs = True
433 433
434 434 from rhodecode.model.user_group import UserGroupModel
435 435
436 436 if handle_mode == 'detach':
437 437 for r in user_groups:
438 438 for user_user_group_to_perm in r.user_user_group_to_perm:
439 439 if user_user_group_to_perm.user.username == username:
440 440 user_user_group_to_perm.user = _superadmin
441 441 r.user = _superadmin
442 442 # set description we know why we super admin now owns
443 443 # additional repositories that were orphaned !
444 444 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
445 445 self.sa.add(r)
446 446 left_overs = False
447 447 elif handle_mode == 'delete':
448 448 for r in user_groups:
449 449 UserGroupModel().delete(r)
450 450 left_overs = False
451 451
452 452 # if nothing is done we have left overs left
453 453 return left_overs
454 454
455 455 def delete(self, user, cur_user=None, handle_repos=None,
456 456 handle_repo_groups=None, handle_user_groups=None):
457 457 if not cur_user:
458 458 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
459 459 user = self._get_user(user)
460 460
461 461 try:
462 462 if user.username == User.DEFAULT_USER:
463 463 raise DefaultUserException(
464 464 _(u"You can't remove this user since it's"
465 465 u" crucial for entire application"))
466 466
467 467 left_overs = self._handle_user_repos(
468 468 user.username, user.repositories, handle_repos)
469 469 if left_overs and user.repositories:
470 470 repos = [x.repo_name for x in user.repositories]
471 471 raise UserOwnsReposException(
472 472 _(u'user "%s" still owns %s repositories and cannot be '
473 473 u'removed. Switch owners or remove those repositories:%s')
474 474 % (user.username, len(repos), ', '.join(repos)))
475 475
476 476 left_overs = self._handle_user_repo_groups(
477 477 user.username, user.repository_groups, handle_repo_groups)
478 478 if left_overs and user.repository_groups:
479 479 repo_groups = [x.group_name for x in user.repository_groups]
480 480 raise UserOwnsRepoGroupsException(
481 481 _(u'user "%s" still owns %s repository groups and cannot be '
482 482 u'removed. Switch owners or remove those repository groups:%s')
483 483 % (user.username, len(repo_groups), ', '.join(repo_groups)))
484 484
485 485 left_overs = self._handle_user_user_groups(
486 486 user.username, user.user_groups, handle_user_groups)
487 487 if left_overs and user.user_groups:
488 488 user_groups = [x.users_group_name for x in user.user_groups]
489 489 raise UserOwnsUserGroupsException(
490 490 _(u'user "%s" still owns %s user groups and cannot be '
491 491 u'removed. Switch owners or remove those user groups:%s')
492 492 % (user.username, len(user_groups), ', '.join(user_groups)))
493 493
494 494 # we might change the user data with detach/delete, make sure
495 495 # the object is marked as expired before actually deleting !
496 496 self.sa.expire(user)
497 497 self.sa.delete(user)
498 498 from rhodecode.lib.hooks_base import log_delete_user
499 499 log_delete_user(deleted_by=cur_user, **user.get_dict())
500 500 except Exception:
501 501 log.error(traceback.format_exc())
502 502 raise
503 503
504 504 def reset_password_link(self, data, pwd_reset_url):
505 505 from rhodecode.lib.celerylib import tasks, run_task
506 506 from rhodecode.model.notification import EmailNotificationModel
507 507 user_email = data['email']
508 508 try:
509 509 user = User.get_by_email(user_email)
510 510 if user:
511 511 log.debug('password reset user found %s', user)
512 512
513 513 email_kwargs = {
514 514 'password_reset_url': pwd_reset_url,
515 515 'user': user,
516 516 'email': user_email,
517 517 'date': datetime.datetime.now()
518 518 }
519 519
520 520 (subject, headers, email_body,
521 521 email_body_plaintext) = EmailNotificationModel().render_email(
522 522 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
523 523
524 524 recipients = [user_email]
525 525
526 526 action_logger_generic(
527 527 'sending password reset email to user: {}'.format(
528 528 user), namespace='security.password_reset')
529 529
530 530 run_task(tasks.send_email, recipients, subject,
531 531 email_body_plaintext, email_body)
532 532
533 533 else:
534 534 log.debug("password reset email %s not found", user_email)
535 535 except Exception:
536 536 log.error(traceback.format_exc())
537 537 return False
538 538
539 539 return True
540 540
541 def reset_password(self, data, pwd_reset_url):
541 def reset_password(self, data):
542 542 from rhodecode.lib.celerylib import tasks, run_task
543 543 from rhodecode.model.notification import EmailNotificationModel
544 544 from rhodecode.lib import auth
545 545 user_email = data['email']
546 546 pre_db = True
547 547 try:
548 548 user = User.get_by_email(user_email)
549 549 new_passwd = auth.PasswordGenerator().gen_password(
550 550 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
551 551 if user:
552 552 user.password = auth.get_crypt_password(new_passwd)
553 553 # also force this user to reset his password !
554 554 user.update_userdata(force_password_change=True)
555 555
556 556 Session().add(user)
557
558 # now delete the token in question
559 UserApiKeys = AuthTokenModel.cls
560 UserApiKeys().query().filter(
561 UserApiKeys.api_key == data['token']).delete()
562
557 563 Session().commit()
558 564 log.info('successfully reset password for `%s`', user_email)
565
559 566 if new_passwd is None:
560 567 raise Exception('unable to generate new password')
561 568
562 569 pre_db = False
563 570
564 571 email_kwargs = {
565 572 'new_password': new_passwd,
566 'password_reset_url': pwd_reset_url,
567 573 'user': user,
568 574 'email': user_email,
569 575 'date': datetime.datetime.now()
570 576 }
571 577
572 578 (subject, headers, email_body,
573 579 email_body_plaintext) = EmailNotificationModel().render_email(
574 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, **email_kwargs)
580 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
581 **email_kwargs)
575 582
576 583 recipients = [user_email]
577 584
578 585 action_logger_generic(
579 586 'sent new password to user: {} with email: {}'.format(
580 587 user, user_email), namespace='security.password_reset')
581 588
582 589 run_task(tasks.send_email, recipients, subject,
583 590 email_body_plaintext, email_body)
584 591
585 592 except Exception:
586 593 log.error('Failed to update user password')
587 594 log.error(traceback.format_exc())
588 595 if pre_db:
589 596 # we rollback only if local db stuff fails. If it goes into
590 597 # run_task, we're pass rollback state this wouldn't work then
591 598 Session().rollback()
592 599
593 600 return True
594 601
595 602 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
596 603 """
597 604 Fetches auth_user by user_id,or api_key if present.
598 605 Fills auth_user attributes with those taken from database.
599 606 Additionally set's is_authenitated if lookup fails
600 607 present in database
601 608
602 609 :param auth_user: instance of user to set attributes
603 610 :param user_id: user id to fetch by
604 611 :param api_key: api key to fetch by
605 612 :param username: username to fetch by
606 613 """
607 614 if user_id is None and api_key is None and username is None:
608 615 raise Exception('You need to pass user_id, api_key or username')
609 616
610 617 log.debug(
611 618 'doing fill data based on: user_id:%s api_key:%s username:%s',
612 619 user_id, api_key, username)
613 620 try:
614 621 dbuser = None
615 622 if user_id:
616 623 dbuser = self.get(user_id)
617 624 elif api_key:
618 625 dbuser = self.get_by_auth_token(api_key)
619 626 elif username:
620 627 dbuser = self.get_by_username(username)
621 628
622 629 if not dbuser:
623 630 log.warning(
624 631 'Unable to lookup user by id:%s api_key:%s username:%s',
625 632 user_id, api_key, username)
626 633 return False
627 634 if not dbuser.active:
628 635 log.debug('User `%s` is inactive, skipping fill data', username)
629 636 return False
630 637
631 638 log.debug('filling user:%s data', dbuser)
632 639
633 640 # TODO: johbo: Think about this and find a clean solution
634 641 user_data = dbuser.get_dict()
635 642 user_data.update(dbuser.get_api_data(include_secrets=True))
636 643
637 644 for k, v in user_data.iteritems():
638 645 # properties of auth user we dont update
639 646 if k not in ['auth_tokens', 'permissions']:
640 647 setattr(auth_user, k, v)
641 648
642 649 # few extras
643 650 setattr(auth_user, 'feed_token', dbuser.feed_token)
644 651 except Exception:
645 652 log.error(traceback.format_exc())
646 653 auth_user.is_authenticated = False
647 654 return False
648 655
649 656 return True
650 657
651 658 def has_perm(self, user, perm):
652 659 perm = self._get_perm(perm)
653 660 user = self._get_user(user)
654 661
655 662 return UserToPerm.query().filter(UserToPerm.user == user)\
656 663 .filter(UserToPerm.permission == perm).scalar() is not None
657 664
658 665 def grant_perm(self, user, perm):
659 666 """
660 667 Grant user global permissions
661 668
662 669 :param user:
663 670 :param perm:
664 671 """
665 672 user = self._get_user(user)
666 673 perm = self._get_perm(perm)
667 674 # if this permission is already granted skip it
668 675 _perm = UserToPerm.query()\
669 676 .filter(UserToPerm.user == user)\
670 677 .filter(UserToPerm.permission == perm)\
671 678 .scalar()
672 679 if _perm:
673 680 return
674 681 new = UserToPerm()
675 682 new.user = user
676 683 new.permission = perm
677 684 self.sa.add(new)
678 685 return new
679 686
680 687 def revoke_perm(self, user, perm):
681 688 """
682 689 Revoke users global permissions
683 690
684 691 :param user:
685 692 :param perm:
686 693 """
687 694 user = self._get_user(user)
688 695 perm = self._get_perm(perm)
689 696
690 697 obj = UserToPerm.query()\
691 698 .filter(UserToPerm.user == user)\
692 699 .filter(UserToPerm.permission == perm)\
693 700 .scalar()
694 701 if obj:
695 702 self.sa.delete(obj)
696 703
697 704 def add_extra_email(self, user, email):
698 705 """
699 706 Adds email address to UserEmailMap
700 707
701 708 :param user:
702 709 :param email:
703 710 """
704 711 from rhodecode.model import forms
705 712 form = forms.UserExtraEmailForm()()
706 713 data = form.to_python({'email': email})
707 714 user = self._get_user(user)
708 715
709 716 obj = UserEmailMap()
710 717 obj.user = user
711 718 obj.email = data['email']
712 719 self.sa.add(obj)
713 720 return obj
714 721
715 722 def delete_extra_email(self, user, email_id):
716 723 """
717 724 Removes email address from UserEmailMap
718 725
719 726 :param user:
720 727 :param email_id:
721 728 """
722 729 user = self._get_user(user)
723 730 obj = UserEmailMap.query().get(email_id)
724 731 if obj:
725 732 self.sa.delete(obj)
726 733
727 734 def parse_ip_range(self, ip_range):
728 735 ip_list = []
729 736 def make_unique(value):
730 737 seen = []
731 738 return [c for c in value if not (c in seen or seen.append(c))]
732 739
733 740 # firsts split by commas
734 741 for ip_range in ip_range.split(','):
735 742 if not ip_range:
736 743 continue
737 744 ip_range = ip_range.strip()
738 745 if '-' in ip_range:
739 746 start_ip, end_ip = ip_range.split('-', 1)
740 747 start_ip = ipaddress.ip_address(start_ip.strip())
741 748 end_ip = ipaddress.ip_address(end_ip.strip())
742 749 parsed_ip_range = []
743 750
744 751 for index in xrange(int(start_ip), int(end_ip) + 1):
745 752 new_ip = ipaddress.ip_address(index)
746 753 parsed_ip_range.append(str(new_ip))
747 754 ip_list.extend(parsed_ip_range)
748 755 else:
749 756 ip_list.append(ip_range)
750 757
751 758 return make_unique(ip_list)
752 759
753 760 def add_extra_ip(self, user, ip, description=None):
754 761 """
755 762 Adds ip address to UserIpMap
756 763
757 764 :param user:
758 765 :param ip:
759 766 """
760 767 from rhodecode.model import forms
761 768 form = forms.UserExtraIpForm()()
762 769 data = form.to_python({'ip': ip})
763 770 user = self._get_user(user)
764 771
765 772 obj = UserIpMap()
766 773 obj.user = user
767 774 obj.ip_addr = data['ip']
768 775 obj.description = description
769 776 self.sa.add(obj)
770 777 return obj
771 778
772 779 def delete_extra_ip(self, user, ip_id):
773 780 """
774 781 Removes ip address from UserIpMap
775 782
776 783 :param user:
777 784 :param ip_id:
778 785 """
779 786 user = self._get_user(user)
780 787 obj = UserIpMap.query().get(ip_id)
781 788 if obj:
782 789 self.sa.delete(obj)
783 790
784 791 def get_accounts_in_creation_order(self, current_user=None):
785 792 """
786 793 Get accounts in order of creation for deactivation for license limits
787 794
788 795 pick currently logged in user, and append to the list in position 0
789 796 pick all super-admins in order of creation date and add it to the list
790 797 pick all other accounts in order of creation and add it to the list.
791 798
792 799 Based on that list, the last accounts can be disabled as they are
793 800 created at the end and don't include any of the super admins as well
794 801 as the current user.
795 802
796 803 :param current_user: optionally current user running this operation
797 804 """
798 805
799 806 if not current_user:
800 807 current_user = get_current_rhodecode_user()
801 808 active_super_admins = [
802 809 x.user_id for x in User.query()
803 810 .filter(User.user_id != current_user.user_id)
804 811 .filter(User.active == true())
805 812 .filter(User.admin == true())
806 813 .order_by(User.created_on.asc())]
807 814
808 815 active_regular_users = [
809 816 x.user_id for x in User.query()
810 817 .filter(User.user_id != current_user.user_id)
811 818 .filter(User.active == true())
812 819 .filter(User.admin == false())
813 820 .order_by(User.created_on.asc())]
814 821
815 822 list_of_accounts = [current_user.user_id]
816 823 list_of_accounts += active_super_admins
817 824 list_of_accounts += active_regular_users
818 825
819 826 return list_of_accounts
820 827
821 828 def deactivate_last_users(self, expected_users):
822 829 """
823 830 Deactivate accounts that are over the license limits.
824 831 Algorithm of which accounts to disabled is based on the formula:
825 832
826 833 Get current user, then super admins in creation order, then regular
827 834 active users in creation order.
828 835
829 836 Using that list we mark all accounts from the end of it as inactive.
830 837 This way we block only latest created accounts.
831 838
832 839 :param expected_users: list of users in special order, we deactivate
833 840 the end N ammoun of users from that list
834 841 """
835 842
836 843 list_of_accounts = self.get_accounts_in_creation_order()
837 844
838 845 for acc_id in list_of_accounts[expected_users + 1:]:
839 846 user = User.get(acc_id)
840 847 log.info('Deactivating account %s for license unlock', user)
841 848 user.active = False
842 849 Session().add(user)
843 850 Session().commit()
844 851
845 852 return
@@ -1,31 +1,33 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 4 <%def name="subject()" filter="n,trim">
5 5 RhodeCode Password reset
6 6 </%def>
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 Hi ${user.username},
11 11
12 12 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
13 13
14 14 *If you didn't do this, please contact your RhodeCode administrator.*
15 15
16 16 You can continue, and generate new password by clicking following URL:
17 17 ${password_reset_url}
18 18
19 This link will be active for 10 minutes.
19 20 ${self.plaintext_footer()}
20 21 </%def>
21 22
22 23 ## BODY GOES BELOW
23 24 <p>
24 25 Hello ${user.username},
25 26 </p><p>
26 27 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
27 28 <br/>
28 29 <strong>If you did not request a password reset, please contact your RhodeCode administrator.</strong>
29 30 </p><p>
30 31 <a href="${password_reset_url}">${_('Generate new password here')}.</a>
32 This link will be active for 10 minutes.
31 33 </p>
@@ -1,30 +1,29 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 4 <%def name="subject()" filter="n,trim">
5 5 Your new RhodeCode password
6 6 </%def>
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 Hi ${user.username},
11 11
12 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
12 Below is your new access password for RhodeCode.
13 13
14 14 *If you didn't do this, please contact your RhodeCode administrator.*
15 15
16 You can continue, and generate new password by clicking following URL:
17 ${password_reset_url}
16 password: ${new_password}
18 17
19 18 ${self.plaintext_footer()}
20 19 </%def>
21 20
22 21 ## BODY GOES BELOW
23 22 <p>
24 23 Hello ${user.username},
25 24 </p><p>
26 25 Below is your new access password for RhodeCode.
27 26 <br/>
28 27 <strong>If you didn't request a new password, please contact your RhodeCode administrator.</strong>
29 28 </p>
30 <p>password: <input value='${new_password}'/></p>
29 <p>password: <pre>${new_password}</pre>
@@ -1,591 +1,510 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 27 from rhodecode.tests import (
28 TestController, assert_session_flash, clear_all_caches, url,
29 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN)
30 29 from rhodecode.tests.fixture import Fixture
31 30 from rhodecode.tests.utils import AssertResponse, get_session_from_response
32 from rhodecode.lib.auth import check_password, generate_auth_token
33 from rhodecode.lib import helpers as h
31 from rhodecode.lib.auth import check_password
34 32 from rhodecode.model.auth_token import AuthTokenModel
35 33 from rhodecode.model import validators
36 from rhodecode.model.db import User, Notification
34 from rhodecode.model.db import User, Notification, UserApiKeys
37 35 from rhodecode.model.meta import Session
38 36
39 37 fixture = Fixture()
40 38
41 39 # Hardcode URLs because we don't have a request object to use
42 40 # pyramids URL generation methods.
43 41 index_url = '/'
44 42 login_url = ADMIN_PREFIX + '/login'
45 43 logut_url = ADMIN_PREFIX + '/logout'
46 44 register_url = ADMIN_PREFIX + '/register'
47 45 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
48 46 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
49 47
50 48
51 49 @pytest.mark.usefixtures('app')
52 class TestLoginController:
50 class TestLoginController(object):
53 51 destroy_users = set()
54 52
55 53 @classmethod
56 54 def teardown_class(cls):
57 55 fixture.destroy_users(cls.destroy_users)
58 56
59 57 def teardown_method(self, method):
60 58 for n in Notification.query().all():
61 59 Session().delete(n)
62 60
63 61 Session().commit()
64 62 assert Notification.query().all() == []
65 63
66 64 def test_index(self):
67 65 response = self.app.get(login_url)
68 66 assert response.status == '200 OK'
69 67 # Test response...
70 68
71 69 def test_login_admin_ok(self):
72 70 response = self.app.post(login_url,
73 71 {'username': 'test_admin',
74 72 'password': 'test12'})
75 73 assert response.status == '302 Found'
76 74 session = get_session_from_response(response)
77 75 username = session['rhodecode_user'].get('username')
78 76 assert username == 'test_admin'
79 77 response = response.follow()
80 78 response.mustcontain('/%s' % HG_REPO)
81 79
82 80 def test_login_regular_ok(self):
83 81 response = self.app.post(login_url,
84 82 {'username': 'test_regular',
85 83 'password': 'test12'})
86 84
87 85 assert response.status == '302 Found'
88 86 session = get_session_from_response(response)
89 87 username = session['rhodecode_user'].get('username')
90 88 assert username == 'test_regular'
91 89 response = response.follow()
92 90 response.mustcontain('/%s' % HG_REPO)
93 91
94 92 def test_login_ok_came_from(self):
95 93 test_came_from = '/_admin/users?branch=stable'
96 94 _url = '{}?came_from={}'.format(login_url, test_came_from)
97 95 response = self.app.post(
98 96 _url, {'username': 'test_admin', 'password': 'test12'})
99 97 assert response.status == '302 Found'
100 98 assert 'branch=stable' in response.location
101 99 response = response.follow()
102 100
103 101 assert response.status == '200 OK'
104 102 response.mustcontain('Users administration')
105 103
106 104 def test_redirect_to_login_with_get_args(self):
107 105 with fixture.anon_access(False):
108 106 kwargs = {'branch': 'stable'}
109 107 response = self.app.get(
110 108 url('summary_home', repo_name=HG_REPO, **kwargs))
111 109 assert response.status == '302 Found'
112 110 response_query = urlparse.parse_qsl(response.location)
113 111 assert 'branch=stable' in response_query[0][1]
114 112
115 113 def test_login_form_with_get_args(self):
116 114 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
117 115 response = self.app.get(_url)
118 116 assert 'branch%3Dstable' in response.form.action
119 117
120 118 @pytest.mark.parametrize("url_came_from", [
121 119 'data:text/html,<script>window.alert("xss")</script>',
122 120 'mailto:test@rhodecode.org',
123 121 'file:///etc/passwd',
124 122 'ftp://some.ftp.server',
125 123 'http://other.domain',
126 124 '/\r\nX-Forwarded-Host: http://example.org',
127 125 ])
128 126 def test_login_bad_came_froms(self, url_came_from):
129 127 _url = '{}?came_from={}'.format(login_url, url_came_from)
130 128 response = self.app.post(
131 129 _url,
132 130 {'username': 'test_admin', 'password': 'test12'})
133 131 assert response.status == '302 Found'
134 132 response = response.follow()
135 133 assert response.status == '200 OK'
136 134 assert response.request.path == '/'
137 135
138 136 def test_login_short_password(self):
139 137 response = self.app.post(login_url,
140 138 {'username': 'test_admin',
141 139 'password': 'as'})
142 140 assert response.status == '200 OK'
143 141
144 142 response.mustcontain('Enter 3 characters or more')
145 143
146 144 def test_login_wrong_non_ascii_password(self, user_regular):
147 145 response = self.app.post(
148 146 login_url,
149 147 {'username': user_regular.username,
150 148 'password': u'invalid-non-asci\xe4'.encode('utf8')})
151 149
152 150 response.mustcontain('invalid user name')
153 151 response.mustcontain('invalid password')
154 152
155 153 def test_login_with_non_ascii_password(self, user_util):
156 154 password = u'valid-non-ascii\xe4'
157 155 user = user_util.create_user(password=password)
158 156 response = self.app.post(
159 157 login_url,
160 158 {'username': user.username,
161 159 'password': password.encode('utf-8')})
162 160 assert response.status_code == 302
163 161
164 162 def test_login_wrong_username_password(self):
165 163 response = self.app.post(login_url,
166 164 {'username': 'error',
167 165 'password': 'test12'})
168 166
169 167 response.mustcontain('invalid user name')
170 168 response.mustcontain('invalid password')
171 169
172 170 def test_login_admin_ok_password_migration(self, real_crypto_backend):
173 171 from rhodecode.lib import auth
174 172
175 173 # create new user, with sha256 password
176 174 temp_user = 'test_admin_sha256'
177 175 user = fixture.create_user(temp_user)
178 176 user.password = auth._RhodeCodeCryptoSha256().hash_create(
179 177 b'test123')
180 178 Session().add(user)
181 179 Session().commit()
182 180 self.destroy_users.add(temp_user)
183 181 response = self.app.post(login_url,
184 182 {'username': temp_user,
185 183 'password': 'test123'})
186 184
187 185 assert response.status == '302 Found'
188 186 session = get_session_from_response(response)
189 187 username = session['rhodecode_user'].get('username')
190 188 assert username == temp_user
191 189 response = response.follow()
192 190 response.mustcontain('/%s' % HG_REPO)
193 191
194 192 # new password should be bcrypted, after log-in and transfer
195 193 user = User.get_by_username(temp_user)
196 194 assert user.password.startswith('$')
197 195
198 196 # REGISTRATIONS
199 197 def test_register(self):
200 198 response = self.app.get(register_url)
201 199 response.mustcontain('Create an Account')
202 200
203 201 def test_register_err_same_username(self):
204 202 uname = 'test_admin'
205 203 response = self.app.post(
206 204 register_url,
207 205 {
208 206 'username': uname,
209 207 'password': 'test12',
210 208 'password_confirmation': 'test12',
211 209 'email': 'goodmail@domain.com',
212 210 'firstname': 'test',
213 211 'lastname': 'test'
214 212 }
215 213 )
216 214
217 215 assertr = AssertResponse(response)
218 216 msg = validators.ValidUsername()._messages['username_exists']
219 217 msg = msg % {'username': uname}
220 218 assertr.element_contains('#username+.error-message', msg)
221 219
222 220 def test_register_err_same_email(self):
223 221 response = self.app.post(
224 222 register_url,
225 223 {
226 224 'username': 'test_admin_0',
227 225 'password': 'test12',
228 226 'password_confirmation': 'test12',
229 227 'email': 'test_admin@mail.com',
230 228 'firstname': 'test',
231 229 'lastname': 'test'
232 230 }
233 231 )
234 232
235 233 assertr = AssertResponse(response)
236 234 msg = validators.UniqSystemEmail()()._messages['email_taken']
237 235 assertr.element_contains('#email+.error-message', msg)
238 236
239 237 def test_register_err_same_email_case_sensitive(self):
240 238 response = self.app.post(
241 239 register_url,
242 240 {
243 241 'username': 'test_admin_1',
244 242 'password': 'test12',
245 243 'password_confirmation': 'test12',
246 244 'email': 'TesT_Admin@mail.COM',
247 245 'firstname': 'test',
248 246 'lastname': 'test'
249 247 }
250 248 )
251 249 assertr = AssertResponse(response)
252 250 msg = validators.UniqSystemEmail()()._messages['email_taken']
253 251 assertr.element_contains('#email+.error-message', msg)
254 252
255 253 def test_register_err_wrong_data(self):
256 254 response = self.app.post(
257 255 register_url,
258 256 {
259 257 'username': 'xs',
260 258 'password': 'test',
261 259 'password_confirmation': 'test',
262 260 'email': 'goodmailm',
263 261 'firstname': 'test',
264 262 'lastname': 'test'
265 263 }
266 264 )
267 265 assert response.status == '200 OK'
268 266 response.mustcontain('An email address must contain a single @')
269 267 response.mustcontain('Enter a value 6 characters long or more')
270 268
271 269 def test_register_err_username(self):
272 270 response = self.app.post(
273 271 register_url,
274 272 {
275 273 'username': 'error user',
276 274 'password': 'test12',
277 275 'password_confirmation': 'test12',
278 276 'email': 'goodmailm',
279 277 'firstname': 'test',
280 278 'lastname': 'test'
281 279 }
282 280 )
283 281
284 282 response.mustcontain('An email address must contain a single @')
285 283 response.mustcontain(
286 284 'Username may only contain '
287 285 'alphanumeric characters underscores, '
288 286 'periods or dashes and must begin with '
289 287 'alphanumeric character')
290 288
291 289 def test_register_err_case_sensitive(self):
292 290 usr = 'Test_Admin'
293 291 response = self.app.post(
294 292 register_url,
295 293 {
296 294 'username': usr,
297 295 'password': 'test12',
298 296 'password_confirmation': 'test12',
299 297 'email': 'goodmailm',
300 298 'firstname': 'test',
301 299 'lastname': 'test'
302 300 }
303 301 )
304 302
305 303 assertr = AssertResponse(response)
306 304 msg = validators.ValidUsername()._messages['username_exists']
307 305 msg = msg % {'username': usr}
308 306 assertr.element_contains('#username+.error-message', msg)
309 307
310 308 def test_register_special_chars(self):
311 309 response = self.app.post(
312 310 register_url,
313 311 {
314 312 'username': 'xxxaxn',
315 313 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
316 314 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
317 315 'email': 'goodmailm@test.plx',
318 316 'firstname': 'test',
319 317 'lastname': 'test'
320 318 }
321 319 )
322 320
323 321 msg = validators.ValidPassword()._messages['invalid_password']
324 322 response.mustcontain(msg)
325 323
326 324 def test_register_password_mismatch(self):
327 325 response = self.app.post(
328 326 register_url,
329 327 {
330 328 'username': 'xs',
331 329 'password': '123qwe',
332 330 'password_confirmation': 'qwe123',
333 331 'email': 'goodmailm@test.plxa',
334 332 'firstname': 'test',
335 333 'lastname': 'test'
336 334 }
337 335 )
338 336 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
339 337 response.mustcontain(msg)
340 338
341 339 def test_register_ok(self):
342 340 username = 'test_regular4'
343 341 password = 'qweqwe'
344 342 email = 'marcin@test.com'
345 343 name = 'testname'
346 344 lastname = 'testlastname'
347 345
348 346 response = self.app.post(
349 347 register_url,
350 348 {
351 349 'username': username,
352 350 'password': password,
353 351 'password_confirmation': password,
354 352 'email': email,
355 353 'firstname': name,
356 354 'lastname': lastname,
357 355 'admin': True
358 356 }
359 357 ) # This should be overriden
360 358 assert response.status == '302 Found'
361 359 assert_session_flash(
362 360 response, 'You have successfully registered with RhodeCode')
363 361
364 362 ret = Session().query(User).filter(
365 363 User.username == 'test_regular4').one()
366 364 assert ret.username == username
367 365 assert check_password(password, ret.password)
368 366 assert ret.email == email
369 367 assert ret.name == name
370 368 assert ret.lastname == lastname
371 369 assert ret.api_key is not None
372 370 assert not ret.admin
373 371
374 372 def test_forgot_password_wrong_mail(self):
375 373 bad_email = 'marcin@wrongmail.org'
376 374 response = self.app.post(
377 pwd_reset_url,
378 {'email': bad_email, }
375 pwd_reset_url, {'email': bad_email, }
379 376 )
377 assert_session_flash(response,
378 'If such email exists, a password reset link was sent to it.')
380 379
381 msg = validators.ValidSystemEmail()._messages['non_existing_email']
382 msg = h.html_escape(msg % {'email': bad_email})
383 response.mustcontain()
384
385 def test_forgot_password(self):
380 def test_forgot_password(self, user_util):
386 381 response = self.app.get(pwd_reset_url)
387 382 assert response.status == '200 OK'
388 383
389 username = 'test_password_reset_1'
390 password = 'qweqwe'
391 email = 'marcin@python-works.com'
392 name = 'passwd'
393 lastname = 'reset'
384 user = user_util.create_user()
385 user_id = user.user_id
386 email = user.email
394 387
395 new = User()
396 new.username = username
397 new.password = password
398 new.email = email
399 new.name = name
400 new.lastname = lastname
401 new.api_key = generate_auth_token(username)
402 Session().add(new)
403 Session().commit()
388 response = self.app.post(pwd_reset_url, {'email': email, })
404 389
405 response = self.app.post(pwd_reset_url,
406 {'email': email, })
407
408 assert_session_flash(
409 response, 'Your password reset link was sent')
410
411 response = response.follow()
390 assert_session_flash(response,
391 'If such email exists, a password reset link was sent to it.')
412 392
413 393 # BAD KEY
414
415 key = "bad"
416 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
394 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, 'badkey')
417 395 response = self.app.get(confirm_url)
418 396 assert response.status == '302 Found'
419 397 assert response.location.endswith(pwd_reset_url)
398 assert_session_flash(response, 'Given reset token is invalid')
399
400 response.follow() # cleanup flash
420 401
421 402 # GOOD KEY
403 key = UserApiKeys.query()\
404 .filter(UserApiKeys.user_id == user_id)\
405 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
406 .first()
422 407
423 key = User.get_by_username(username).api_key
424 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
408 assert key
409
410 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key.api_key)
425 411 response = self.app.get(confirm_url)
426 412 assert response.status == '302 Found'
427 413 assert response.location.endswith(login_url)
428 414
429 415 assert_session_flash(
430 416 response,
431 417 'Your password reset was successful, '
432 418 'a new password has been sent to your email')
433 419
434 response = response.follow()
420 response.follow()
435 421
436 422 def _get_api_whitelist(self, values=None):
437 423 config = {'api_access_controllers_whitelist': values or []}
438 424 return config
439 425
440 426 @pytest.mark.parametrize("test_name, auth_token", [
441 427 ('none', None),
442 428 ('empty_string', ''),
443 429 ('fake_number', '123456'),
444 430 ('proper_auth_token', None)
445 431 ])
446 432 def test_access_not_whitelisted_page_via_auth_token(
447 433 self, test_name, auth_token, user_admin):
448 434
449 435 whitelist = self._get_api_whitelist([])
450 436 with mock.patch.dict('rhodecode.CONFIG', whitelist):
451 437 assert [] == whitelist['api_access_controllers_whitelist']
452 438 if test_name == 'proper_auth_token':
453 439 # use builtin if api_key is None
454 440 auth_token = user_admin.api_key
455 441
456 442 with fixture.anon_access(False):
457 443 self.app.get(url(controller='changeset',
458 444 action='changeset_raw',
459 445 repo_name=HG_REPO, revision='tip',
460 446 api_key=auth_token),
461 447 status=302)
462 448
463 449 @pytest.mark.parametrize("test_name, auth_token, code", [
464 450 ('none', None, 302),
465 451 ('empty_string', '', 302),
466 452 ('fake_number', '123456', 302),
467 453 ('proper_auth_token', None, 200)
468 454 ])
469 455 def test_access_whitelisted_page_via_auth_token(
470 456 self, test_name, auth_token, code, user_admin):
471 457
472 458 whitelist_entry = ['ChangesetController:changeset_raw']
473 459 whitelist = self._get_api_whitelist(whitelist_entry)
474 460
475 461 with mock.patch.dict('rhodecode.CONFIG', whitelist):
476 462 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
477 463
478 464 if test_name == 'proper_auth_token':
479 465 auth_token = user_admin.api_key
480 466
481 467 with fixture.anon_access(False):
482 468 self.app.get(url(controller='changeset',
483 469 action='changeset_raw',
484 470 repo_name=HG_REPO, revision='tip',
485 471 api_key=auth_token),
486 472 status=code)
487 473
488 474 def test_access_page_via_extra_auth_token(self):
489 475 whitelist = self._get_api_whitelist(
490 476 ['ChangesetController:changeset_raw'])
491 477 with mock.patch.dict('rhodecode.CONFIG', whitelist):
492 478 assert ['ChangesetController:changeset_raw'] == \
493 479 whitelist['api_access_controllers_whitelist']
494 480
495 481 new_auth_token = AuthTokenModel().create(
496 482 TEST_USER_ADMIN_LOGIN, 'test')
497 483 Session().commit()
498 484 with fixture.anon_access(False):
499 485 self.app.get(url(controller='changeset',
500 486 action='changeset_raw',
501 487 repo_name=HG_REPO, revision='tip',
502 488 api_key=new_auth_token.api_key),
503 489 status=200)
504 490
505 491 def test_access_page_via_expired_auth_token(self):
506 492 whitelist = self._get_api_whitelist(
507 493 ['ChangesetController:changeset_raw'])
508 494 with mock.patch.dict('rhodecode.CONFIG', whitelist):
509 495 assert ['ChangesetController:changeset_raw'] == \
510 496 whitelist['api_access_controllers_whitelist']
511 497
512 498 new_auth_token = AuthTokenModel().create(
513 499 TEST_USER_ADMIN_LOGIN, 'test')
514 500 Session().commit()
515 501 # patch the api key and make it expired
516 502 new_auth_token.expires = 0
517 503 Session().add(new_auth_token)
518 504 Session().commit()
519 505 with fixture.anon_access(False):
520 506 self.app.get(url(controller='changeset',
521 507 action='changeset_raw',
522 508 repo_name=HG_REPO, revision='tip',
523 509 api_key=new_auth_token.api_key),
524 510 status=302)
525
526
527 class TestPasswordReset(TestController):
528
529 @pytest.mark.parametrize(
530 'pwd_reset_setting, show_link, show_reset', [
531 ('hg.password_reset.enabled', True, True),
532 ('hg.password_reset.hidden', False, True),
533 ('hg.password_reset.disabled', False, False),
534 ])
535 def test_password_reset_settings(
536 self, pwd_reset_setting, show_link, show_reset):
537 clear_all_caches()
538 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
539 params = {
540 'csrf_token': self.csrf_token,
541 'anonymous': 'True',
542 'default_register': 'hg.register.auto_activate',
543 'default_register_message': '',
544 'default_password_reset': pwd_reset_setting,
545 'default_extern_activate': 'hg.extern_activate.auto',
546 }
547 resp = self.app.post(url('admin_permissions_application'), params=params)
548 self.logout_user()
549
550 login_page = self.app.get(login_url)
551 asr_login = AssertResponse(login_page)
552 index_page = self.app.get(index_url)
553 asr_index = AssertResponse(index_page)
554
555 if show_link:
556 asr_login.one_element_exists('a.pwd_reset')
557 asr_index.one_element_exists('a.pwd_reset')
558 else:
559 asr_login.no_element_exists('a.pwd_reset')
560 asr_index.no_element_exists('a.pwd_reset')
561
562 pwdreset_page = self.app.get(pwd_reset_url)
563
564 asr_reset = AssertResponse(pwdreset_page)
565 if show_reset:
566 assert 'Send password reset email' in pwdreset_page
567 asr_reset.one_element_exists('#email')
568 asr_reset.one_element_exists('#send')
569 else:
570 assert 'Password reset is disabled.' in pwdreset_page
571 asr_reset.no_element_exists('#email')
572 asr_reset.no_element_exists('#send')
573
574 def test_password_form_disabled(self):
575 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
576 params = {
577 'csrf_token': self.csrf_token,
578 'anonymous': 'True',
579 'default_register': 'hg.register.auto_activate',
580 'default_register_message': '',
581 'default_password_reset': 'hg.password_reset.disabled',
582 'default_extern_activate': 'hg.extern_activate.auto',
583 }
584 self.app.post(url('admin_permissions_application'), params=params)
585 self.logout_user()
586
587 pwdreset_page = self.app.post(
588 pwd_reset_url,
589 {'email': 'lisa@rhodecode.com',}
590 )
591 assert 'Password reset is disabled.' in pwdreset_page
General Comments 0
You need to be logged in to leave comments. Login now