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