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