##// END OF EJS Templates
app: use simpler way to extract default_user_id, this will be now registered at server...
marcink -
r4332:b6d13602 default
parent child Browse files
Show More
@@ -1,299 +1,299 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 mock
22 22 import pytest
23 23 from rhodecode.model.db import User, UserIpMap
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.permission import PermissionModel
26 26 from rhodecode.model.ssh_key import SshKeyModel
27 27 from rhodecode.tests import (
28 28 TestController, clear_cache_regions, assert_session_flash)
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 32 import urllib
33 33 from rhodecode.apps._base import ADMIN_PREFIX
34 34
35 35 base_url = {
36 36 'edit_user_ips':
37 37 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
38 38 'edit_user_ips_add':
39 39 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
40 40 'edit_user_ips_delete':
41 41 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
42 42
43 43 'admin_permissions_application':
44 44 ADMIN_PREFIX + '/permissions/application',
45 45 'admin_permissions_application_update':
46 46 ADMIN_PREFIX + '/permissions/application/update',
47 47
48 48 'admin_permissions_global':
49 49 ADMIN_PREFIX + '/permissions/global',
50 50 'admin_permissions_global_update':
51 51 ADMIN_PREFIX + '/permissions/global/update',
52 52
53 53 'admin_permissions_object':
54 54 ADMIN_PREFIX + '/permissions/object',
55 55 'admin_permissions_object_update':
56 56 ADMIN_PREFIX + '/permissions/object/update',
57 57
58 58 'admin_permissions_ips':
59 59 ADMIN_PREFIX + '/permissions/ips',
60 60 'admin_permissions_overview':
61 61 ADMIN_PREFIX + '/permissions/overview',
62 62
63 63 'admin_permissions_ssh_keys':
64 64 ADMIN_PREFIX + '/permissions/ssh_keys',
65 65 'admin_permissions_ssh_keys_data':
66 66 ADMIN_PREFIX + '/permissions/ssh_keys/data',
67 67 'admin_permissions_ssh_keys_update':
68 68 ADMIN_PREFIX + '/permissions/ssh_keys/update'
69 69
70 70 }[name].format(**kwargs)
71 71
72 72 if params:
73 73 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
74 74 return base_url
75 75
76 76
77 77 class TestAdminPermissionsController(TestController):
78 78
79 79 @pytest.fixture(scope='class', autouse=True)
80 80 def prepare(self, request):
81 81 # cleanup and reset to default permissions after
82 82 @request.addfinalizer
83 83 def cleanup():
84 84 PermissionModel().create_default_user_permissions(
85 85 User.get_default_user(), force=True)
86 86
87 87 def test_index_application(self):
88 88 self.log_user()
89 89 self.app.get(route_path('admin_permissions_application'))
90 90
91 91 @pytest.mark.parametrize(
92 92 'anonymous, default_register, default_register_message, default_password_reset,'
93 93 'default_extern_activate, expect_error, expect_form_error', [
94 94 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
95 95 False, False),
96 96 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
97 97 False, False),
98 98 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 99 False, False),
100 100 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 101 False, False),
102 102 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
103 103 False, True),
104 104 (True, '', '', 'hg.password_reset.enabled', '', True, False),
105 105 ])
106 106 def test_update_application_permissions(
107 107 self, anonymous, default_register, default_register_message, default_password_reset,
108 108 default_extern_activate, expect_error, expect_form_error):
109 109
110 110 self.log_user()
111 111
112 112 # TODO: anonymous access set here to False, breaks some other tests
113 113 params = {
114 114 'csrf_token': self.csrf_token,
115 115 'anonymous': anonymous,
116 116 'default_register': default_register,
117 117 'default_register_message': default_register_message,
118 118 'default_password_reset': default_password_reset,
119 119 'default_extern_activate': default_extern_activate,
120 120 }
121 121 response = self.app.post(route_path('admin_permissions_application_update'),
122 122 params=params)
123 123 if expect_form_error:
124 124 assert response.status_int == 200
125 125 response.mustcontain('Value must be one of')
126 126 else:
127 127 if expect_error:
128 128 msg = 'Error occurred during update of permissions'
129 129 else:
130 130 msg = 'Application permissions updated successfully'
131 131 assert_session_flash(response, msg)
132 132
133 133 def test_index_object(self):
134 134 self.log_user()
135 135 self.app.get(route_path('admin_permissions_object'))
136 136
137 137 @pytest.mark.parametrize(
138 138 'repo, repo_group, user_group, expect_error, expect_form_error', [
139 139 ('repository.none', 'group.none', 'usergroup.none', False, False),
140 140 ('repository.read', 'group.read', 'usergroup.read', False, False),
141 141 ('repository.write', 'group.write', 'usergroup.write',
142 142 False, False),
143 143 ('repository.admin', 'group.admin', 'usergroup.admin',
144 144 False, False),
145 145 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
146 146 ('', '', '', True, False),
147 147 ])
148 148 def test_update_object_permissions(self, repo, repo_group, user_group,
149 149 expect_error, expect_form_error):
150 150 self.log_user()
151 151
152 152 params = {
153 153 'csrf_token': self.csrf_token,
154 154 'default_repo_perm': repo,
155 155 'overwrite_default_repo': False,
156 156 'default_group_perm': repo_group,
157 157 'overwrite_default_group': False,
158 158 'default_user_group_perm': user_group,
159 159 'overwrite_default_user_group': False,
160 160 }
161 161 response = self.app.post(route_path('admin_permissions_object_update'),
162 162 params=params)
163 163 if expect_form_error:
164 164 assert response.status_int == 200
165 165 response.mustcontain('Value must be one of')
166 166 else:
167 167 if expect_error:
168 168 msg = 'Error occurred during update of permissions'
169 169 else:
170 170 msg = 'Object permissions updated successfully'
171 171 assert_session_flash(response, msg)
172 172
173 173 def test_index_global(self):
174 174 self.log_user()
175 175 self.app.get(route_path('admin_permissions_global'))
176 176
177 177 @pytest.mark.parametrize(
178 178 'repo_create, repo_create_write, user_group_create, repo_group_create,'
179 179 'fork_create, inherit_default_permissions, expect_error,'
180 180 'expect_form_error', [
181 181 ('hg.create.none', 'hg.create.write_on_repogroup.false',
182 182 'hg.usergroup.create.false', 'hg.repogroup.create.false',
183 183 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
184 184 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
185 185 'hg.usergroup.create.true', 'hg.repogroup.create.true',
186 186 'hg.fork.repository', 'hg.inherit_default_perms.false',
187 187 False, False),
188 188 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
189 189 'hg.usergroup.create.true', 'hg.repogroup.create.true',
190 190 'hg.fork.repository', 'hg.inherit_default_perms.false',
191 191 False, True),
192 192 ('', '', '', '', '', '', True, False),
193 193 ])
194 194 def test_update_global_permissions(
195 195 self, repo_create, repo_create_write, user_group_create,
196 196 repo_group_create, fork_create, inherit_default_permissions,
197 197 expect_error, expect_form_error):
198 198 self.log_user()
199 199
200 200 params = {
201 201 'csrf_token': self.csrf_token,
202 202 'default_repo_create': repo_create,
203 203 'default_repo_create_on_write': repo_create_write,
204 204 'default_user_group_create': user_group_create,
205 205 'default_repo_group_create': repo_group_create,
206 206 'default_fork_create': fork_create,
207 207 'default_inherit_default_permissions': inherit_default_permissions
208 208 }
209 209 response = self.app.post(route_path('admin_permissions_global_update'),
210 210 params=params)
211 211 if expect_form_error:
212 212 assert response.status_int == 200
213 213 response.mustcontain('Value must be one of')
214 214 else:
215 215 if expect_error:
216 216 msg = 'Error occurred during update of permissions'
217 217 else:
218 218 msg = 'Global permissions updated successfully'
219 219 assert_session_flash(response, msg)
220 220
221 221 def test_index_ips(self):
222 222 self.log_user()
223 223 response = self.app.get(route_path('admin_permissions_ips'))
224 224 response.mustcontain('All IP addresses are allowed')
225 225
226 226 def test_add_delete_ips(self):
227 227 clear_cache_regions(['sql_cache_short'])
228 228 self.log_user()
229 229
230 230 # ADD
231 default_user_id = User.get_default_user().user_id
231 default_user_id = User.get_default_user_id()
232 232 self.app.post(
233 233 route_path('edit_user_ips_add', user_id=default_user_id),
234 234 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
235 235
236 236 response = self.app.get(route_path('admin_permissions_ips'))
237 237 response.mustcontain('0.0.0.0/24')
238 238 response.mustcontain('0.0.0.0 - 0.0.0.255')
239 239
240 240 # DELETE
241 default_user_id = User.get_default_user().user_id
241 default_user_id = User.get_default_user_id()
242 242 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
243 243 default_user_id).first().ip_id
244 244
245 245 response = self.app.post(
246 246 route_path('edit_user_ips_delete', user_id=default_user_id),
247 247 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
248 248
249 249 assert_session_flash(response, 'Removed ip address from user whitelist')
250 250
251 251 clear_cache_regions(['sql_cache_short'])
252 252 response = self.app.get(route_path('admin_permissions_ips'))
253 253 response.mustcontain('All IP addresses are allowed')
254 254 response.mustcontain(no=['0.0.0.0/24'])
255 255 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
256 256
257 257 def test_index_overview(self):
258 258 self.log_user()
259 259 self.app.get(route_path('admin_permissions_overview'))
260 260
261 261 def test_ssh_keys(self):
262 262 self.log_user()
263 263 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
264 264
265 265 def test_ssh_keys_data(self, user_util, xhr_header):
266 266 self.log_user()
267 267 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
268 268 extra_environ=xhr_header)
269 269 assert response.json == {u'data': [], u'draw': None,
270 270 u'recordsFiltered': 0, u'recordsTotal': 0}
271 271
272 272 dummy_user = user_util.create_user()
273 273 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
274 274 Session().commit()
275 275 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
276 276 extra_environ=xhr_header)
277 277 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
278 278
279 279 def test_ssh_keys_update(self):
280 280 self.log_user()
281 281 response = self.app.post(
282 282 route_path('admin_permissions_ssh_keys_update'),
283 283 dict(csrf_token=self.csrf_token), status=302)
284 284
285 285 assert_session_flash(
286 286 response, 'Updated SSH keys file')
287 287
288 288 def test_ssh_keys_update_disabled(self):
289 289 self.log_user()
290 290
291 291 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
292 292 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
293 293 return_value=False):
294 294 response = self.app.post(
295 295 route_path('admin_permissions_ssh_keys_update'),
296 296 dict(csrf_token=self.csrf_token), status=302)
297 297
298 298 assert_session_flash(
299 299 response, 'SSH key support is disabled in .ini file') No newline at end of file
@@ -1,519 +1,519 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import formencode.htmlfill
25 25 import datetime
26 26 from pyramid.interfaces import IRoutesMapper
27 27
28 28 from pyramid.view import view_config
29 29 from pyramid.httpexceptions import HTTPFound
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 34 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
35 35 from rhodecode import events
36 36
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 40 from rhodecode.lib.utils2 import aslist, safe_unicode
41 41 from rhodecode.model.db import (
42 42 or_, coalesce, User, UserIpMap, UserSshKeys)
43 43 from rhodecode.model.forms import (
44 44 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.permission import PermissionModel
47 47 from rhodecode.model.settings import SettingsModel
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class AdminPermissionsView(BaseAppView, DataGridAppView):
54 54 def load_default_context(self):
55 55 c = self._get_local_tmpl_context()
56 56 PermissionModel().set_global_permission_choices(
57 57 c, gettext_translator=self.request.translate)
58 58 return c
59 59
60 60 @LoginRequired()
61 61 @HasPermissionAllDecorator('hg.admin')
62 62 @view_config(
63 63 route_name='admin_permissions_application', request_method='GET',
64 64 renderer='rhodecode:templates/admin/permissions/permissions.mako')
65 65 def permissions_application(self):
66 66 c = self.load_default_context()
67 67 c.active = 'application'
68 68
69 69 c.user = User.get_default_user(refresh=True)
70 70
71 71 app_settings = c.rc_config
72 72
73 73 defaults = {
74 74 'anonymous': c.user.active,
75 75 'default_register_message': app_settings.get(
76 76 'rhodecode_register_message')
77 77 }
78 78 defaults.update(c.user.get_default_perms())
79 79
80 80 data = render('rhodecode:templates/admin/permissions/permissions.mako',
81 81 self._get_template_context(c), self.request)
82 82 html = formencode.htmlfill.render(
83 83 data,
84 84 defaults=defaults,
85 85 encoding="UTF-8",
86 86 force_defaults=False
87 87 )
88 88 return Response(html)
89 89
90 90 @LoginRequired()
91 91 @HasPermissionAllDecorator('hg.admin')
92 92 @CSRFRequired()
93 93 @view_config(
94 94 route_name='admin_permissions_application_update', request_method='POST',
95 95 renderer='rhodecode:templates/admin/permissions/permissions.mako')
96 96 def permissions_application_update(self):
97 97 _ = self.request.translate
98 98 c = self.load_default_context()
99 99 c.active = 'application'
100 100
101 101 _form = ApplicationPermissionsForm(
102 102 self.request.translate,
103 103 [x[0] for x in c.register_choices],
104 104 [x[0] for x in c.password_reset_choices],
105 105 [x[0] for x in c.extern_activate_choices])()
106 106
107 107 try:
108 108 form_result = _form.to_python(dict(self.request.POST))
109 109 form_result.update({'perm_user_name': User.DEFAULT_USER})
110 110 PermissionModel().update_application_permissions(form_result)
111 111
112 112 settings = [
113 113 ('register_message', 'default_register_message'),
114 114 ]
115 115 for setting, form_key in settings:
116 116 sett = SettingsModel().create_or_update_setting(
117 117 setting, form_result[form_key])
118 118 Session().add(sett)
119 119
120 120 Session().commit()
121 121 h.flash(_('Application permissions updated successfully'),
122 122 category='success')
123 123
124 124 except formencode.Invalid as errors:
125 125 defaults = errors.value
126 126
127 127 data = render(
128 128 'rhodecode:templates/admin/permissions/permissions.mako',
129 129 self._get_template_context(c), self.request)
130 130 html = formencode.htmlfill.render(
131 131 data,
132 132 defaults=defaults,
133 133 errors=errors.error_dict or {},
134 134 prefix_error=False,
135 135 encoding="UTF-8",
136 136 force_defaults=False
137 137 )
138 138 return Response(html)
139 139
140 140 except Exception:
141 141 log.exception("Exception during update of permissions")
142 142 h.flash(_('Error occurred during update of permissions'),
143 143 category='error')
144 144
145 affected_user_ids = [User.get_default_user().user_id]
145 affected_user_ids = [User.get_default_user_id()]
146 146 PermissionModel().trigger_permission_flush(affected_user_ids)
147 147
148 148 raise HTTPFound(h.route_path('admin_permissions_application'))
149 149
150 150 @LoginRequired()
151 151 @HasPermissionAllDecorator('hg.admin')
152 152 @view_config(
153 153 route_name='admin_permissions_object', request_method='GET',
154 154 renderer='rhodecode:templates/admin/permissions/permissions.mako')
155 155 def permissions_objects(self):
156 156 c = self.load_default_context()
157 157 c.active = 'objects'
158 158
159 159 c.user = User.get_default_user(refresh=True)
160 160 defaults = {}
161 161 defaults.update(c.user.get_default_perms())
162 162
163 163 data = render(
164 164 'rhodecode:templates/admin/permissions/permissions.mako',
165 165 self._get_template_context(c), self.request)
166 166 html = formencode.htmlfill.render(
167 167 data,
168 168 defaults=defaults,
169 169 encoding="UTF-8",
170 170 force_defaults=False
171 171 )
172 172 return Response(html)
173 173
174 174 @LoginRequired()
175 175 @HasPermissionAllDecorator('hg.admin')
176 176 @CSRFRequired()
177 177 @view_config(
178 178 route_name='admin_permissions_object_update', request_method='POST',
179 179 renderer='rhodecode:templates/admin/permissions/permissions.mako')
180 180 def permissions_objects_update(self):
181 181 _ = self.request.translate
182 182 c = self.load_default_context()
183 183 c.active = 'objects'
184 184
185 185 _form = ObjectPermissionsForm(
186 186 self.request.translate,
187 187 [x[0] for x in c.repo_perms_choices],
188 188 [x[0] for x in c.group_perms_choices],
189 189 [x[0] for x in c.user_group_perms_choices],
190 190 )()
191 191
192 192 try:
193 193 form_result = _form.to_python(dict(self.request.POST))
194 194 form_result.update({'perm_user_name': User.DEFAULT_USER})
195 195 PermissionModel().update_object_permissions(form_result)
196 196
197 197 Session().commit()
198 198 h.flash(_('Object permissions updated successfully'),
199 199 category='success')
200 200
201 201 except formencode.Invalid as errors:
202 202 defaults = errors.value
203 203
204 204 data = render(
205 205 'rhodecode:templates/admin/permissions/permissions.mako',
206 206 self._get_template_context(c), self.request)
207 207 html = formencode.htmlfill.render(
208 208 data,
209 209 defaults=defaults,
210 210 errors=errors.error_dict or {},
211 211 prefix_error=False,
212 212 encoding="UTF-8",
213 213 force_defaults=False
214 214 )
215 215 return Response(html)
216 216 except Exception:
217 217 log.exception("Exception during update of permissions")
218 218 h.flash(_('Error occurred during update of permissions'),
219 219 category='error')
220 220
221 affected_user_ids = [User.get_default_user().user_id]
221 affected_user_ids = [User.get_default_user_id()]
222 222 PermissionModel().trigger_permission_flush(affected_user_ids)
223 223
224 224 raise HTTPFound(h.route_path('admin_permissions_object'))
225 225
226 226 @LoginRequired()
227 227 @HasPermissionAllDecorator('hg.admin')
228 228 @view_config(
229 229 route_name='admin_permissions_branch', request_method='GET',
230 230 renderer='rhodecode:templates/admin/permissions/permissions.mako')
231 231 def permissions_branch(self):
232 232 c = self.load_default_context()
233 233 c.active = 'branch'
234 234
235 235 c.user = User.get_default_user(refresh=True)
236 236 defaults = {}
237 237 defaults.update(c.user.get_default_perms())
238 238
239 239 data = render(
240 240 'rhodecode:templates/admin/permissions/permissions.mako',
241 241 self._get_template_context(c), self.request)
242 242 html = formencode.htmlfill.render(
243 243 data,
244 244 defaults=defaults,
245 245 encoding="UTF-8",
246 246 force_defaults=False
247 247 )
248 248 return Response(html)
249 249
250 250 @LoginRequired()
251 251 @HasPermissionAllDecorator('hg.admin')
252 252 @view_config(
253 253 route_name='admin_permissions_global', request_method='GET',
254 254 renderer='rhodecode:templates/admin/permissions/permissions.mako')
255 255 def permissions_global(self):
256 256 c = self.load_default_context()
257 257 c.active = 'global'
258 258
259 259 c.user = User.get_default_user(refresh=True)
260 260 defaults = {}
261 261 defaults.update(c.user.get_default_perms())
262 262
263 263 data = render(
264 264 'rhodecode:templates/admin/permissions/permissions.mako',
265 265 self._get_template_context(c), self.request)
266 266 html = formencode.htmlfill.render(
267 267 data,
268 268 defaults=defaults,
269 269 encoding="UTF-8",
270 270 force_defaults=False
271 271 )
272 272 return Response(html)
273 273
274 274 @LoginRequired()
275 275 @HasPermissionAllDecorator('hg.admin')
276 276 @CSRFRequired()
277 277 @view_config(
278 278 route_name='admin_permissions_global_update', request_method='POST',
279 279 renderer='rhodecode:templates/admin/permissions/permissions.mako')
280 280 def permissions_global_update(self):
281 281 _ = self.request.translate
282 282 c = self.load_default_context()
283 283 c.active = 'global'
284 284
285 285 _form = UserPermissionsForm(
286 286 self.request.translate,
287 287 [x[0] for x in c.repo_create_choices],
288 288 [x[0] for x in c.repo_create_on_write_choices],
289 289 [x[0] for x in c.repo_group_create_choices],
290 290 [x[0] for x in c.user_group_create_choices],
291 291 [x[0] for x in c.fork_choices],
292 292 [x[0] for x in c.inherit_default_permission_choices])()
293 293
294 294 try:
295 295 form_result = _form.to_python(dict(self.request.POST))
296 296 form_result.update({'perm_user_name': User.DEFAULT_USER})
297 297 PermissionModel().update_user_permissions(form_result)
298 298
299 299 Session().commit()
300 300 h.flash(_('Global permissions updated successfully'),
301 301 category='success')
302 302
303 303 except formencode.Invalid as errors:
304 304 defaults = errors.value
305 305
306 306 data = render(
307 307 'rhodecode:templates/admin/permissions/permissions.mako',
308 308 self._get_template_context(c), self.request)
309 309 html = formencode.htmlfill.render(
310 310 data,
311 311 defaults=defaults,
312 312 errors=errors.error_dict or {},
313 313 prefix_error=False,
314 314 encoding="UTF-8",
315 315 force_defaults=False
316 316 )
317 317 return Response(html)
318 318 except Exception:
319 319 log.exception("Exception during update of permissions")
320 320 h.flash(_('Error occurred during update of permissions'),
321 321 category='error')
322 322
323 affected_user_ids = [User.get_default_user().user_id]
323 affected_user_ids = [User.get_default_user_id()]
324 324 PermissionModel().trigger_permission_flush(affected_user_ids)
325 325
326 326 raise HTTPFound(h.route_path('admin_permissions_global'))
327 327
328 328 @LoginRequired()
329 329 @HasPermissionAllDecorator('hg.admin')
330 330 @view_config(
331 331 route_name='admin_permissions_ips', request_method='GET',
332 332 renderer='rhodecode:templates/admin/permissions/permissions.mako')
333 333 def permissions_ips(self):
334 334 c = self.load_default_context()
335 335 c.active = 'ips'
336 336
337 337 c.user = User.get_default_user(refresh=True)
338 338 c.user_ip_map = (
339 339 UserIpMap.query().filter(UserIpMap.user == c.user).all())
340 340
341 341 return self._get_template_context(c)
342 342
343 343 @LoginRequired()
344 344 @HasPermissionAllDecorator('hg.admin')
345 345 @view_config(
346 346 route_name='admin_permissions_overview', request_method='GET',
347 347 renderer='rhodecode:templates/admin/permissions/permissions.mako')
348 348 def permissions_overview(self):
349 349 c = self.load_default_context()
350 350 c.active = 'perms'
351 351
352 352 c.user = User.get_default_user(refresh=True)
353 353 c.perm_user = c.user.AuthUser()
354 354 return self._get_template_context(c)
355 355
356 356 @LoginRequired()
357 357 @HasPermissionAllDecorator('hg.admin')
358 358 @view_config(
359 359 route_name='admin_permissions_auth_token_access', request_method='GET',
360 360 renderer='rhodecode:templates/admin/permissions/permissions.mako')
361 361 def auth_token_access(self):
362 362 from rhodecode import CONFIG
363 363
364 364 c = self.load_default_context()
365 365 c.active = 'auth_token_access'
366 366
367 367 c.user = User.get_default_user(refresh=True)
368 368 c.perm_user = c.user.AuthUser()
369 369
370 370 mapper = self.request.registry.queryUtility(IRoutesMapper)
371 371 c.view_data = []
372 372
373 373 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
374 374 introspector = self.request.registry.introspector
375 375
376 376 view_intr = {}
377 377 for view_data in introspector.get_category('views'):
378 378 intr = view_data['introspectable']
379 379
380 380 if 'route_name' in intr and intr['attr']:
381 381 view_intr[intr['route_name']] = '{}:{}'.format(
382 382 str(intr['derived_callable'].func_name), intr['attr']
383 383 )
384 384
385 385 c.whitelist_key = 'api_access_controllers_whitelist'
386 386 c.whitelist_file = CONFIG.get('__file__')
387 387 whitelist_views = aslist(
388 388 CONFIG.get(c.whitelist_key), sep=',')
389 389
390 390 for route_info in mapper.get_routes():
391 391 if not route_info.name.startswith('__'):
392 392 routepath = route_info.pattern
393 393
394 394 def replace(matchobj):
395 395 if matchobj.group(1):
396 396 return "{%s}" % matchobj.group(1).split(':')[0]
397 397 else:
398 398 return "{%s}" % matchobj.group(2)
399 399
400 400 routepath = _argument_prog.sub(replace, routepath)
401 401
402 402 if not routepath.startswith('/'):
403 403 routepath = '/' + routepath
404 404
405 405 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
406 406 active = view_fqn in whitelist_views
407 407 c.view_data.append((route_info.name, view_fqn, routepath, active))
408 408
409 409 c.whitelist_views = whitelist_views
410 410 return self._get_template_context(c)
411 411
412 412 def ssh_enabled(self):
413 413 return self.request.registry.settings.get(
414 414 'ssh.generate_authorized_keyfile')
415 415
416 416 @LoginRequired()
417 417 @HasPermissionAllDecorator('hg.admin')
418 418 @view_config(
419 419 route_name='admin_permissions_ssh_keys', request_method='GET',
420 420 renderer='rhodecode:templates/admin/permissions/permissions.mako')
421 421 def ssh_keys(self):
422 422 c = self.load_default_context()
423 423 c.active = 'ssh_keys'
424 424 c.ssh_enabled = self.ssh_enabled()
425 425 return self._get_template_context(c)
426 426
427 427 @LoginRequired()
428 428 @HasPermissionAllDecorator('hg.admin')
429 429 @view_config(
430 430 route_name='admin_permissions_ssh_keys_data', request_method='GET',
431 431 renderer='json_ext', xhr=True)
432 432 def ssh_keys_data(self):
433 433 _ = self.request.translate
434 434 self.load_default_context()
435 435 column_map = {
436 436 'fingerprint': 'ssh_key_fingerprint',
437 437 'username': User.username
438 438 }
439 439 draw, start, limit = self._extract_chunk(self.request)
440 440 search_q, order_by, order_dir = self._extract_ordering(
441 441 self.request, column_map=column_map)
442 442
443 443 ssh_keys_data_total_count = UserSshKeys.query()\
444 444 .count()
445 445
446 446 # json generate
447 447 base_q = UserSshKeys.query().join(UserSshKeys.user)
448 448
449 449 if search_q:
450 450 like_expression = u'%{}%'.format(safe_unicode(search_q))
451 451 base_q = base_q.filter(or_(
452 452 User.username.ilike(like_expression),
453 453 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
454 454 ))
455 455
456 456 users_data_total_filtered_count = base_q.count()
457 457
458 458 sort_col = self._get_order_col(order_by, UserSshKeys)
459 459 if sort_col:
460 460 if order_dir == 'asc':
461 461 # handle null values properly to order by NULL last
462 462 if order_by in ['created_on']:
463 463 sort_col = coalesce(sort_col, datetime.date.max)
464 464 sort_col = sort_col.asc()
465 465 else:
466 466 # handle null values properly to order by NULL last
467 467 if order_by in ['created_on']:
468 468 sort_col = coalesce(sort_col, datetime.date.min)
469 469 sort_col = sort_col.desc()
470 470
471 471 base_q = base_q.order_by(sort_col)
472 472 base_q = base_q.offset(start).limit(limit)
473 473
474 474 ssh_keys = base_q.all()
475 475
476 476 ssh_keys_data = []
477 477 for ssh_key in ssh_keys:
478 478 ssh_keys_data.append({
479 479 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
480 480 "fingerprint": ssh_key.ssh_key_fingerprint,
481 481 "description": ssh_key.description,
482 482 "created_on": h.format_date(ssh_key.created_on),
483 483 "accessed_on": h.format_date(ssh_key.accessed_on),
484 484 "action": h.link_to(
485 485 _('Edit'), h.route_path('edit_user_ssh_keys',
486 486 user_id=ssh_key.user.user_id))
487 487 })
488 488
489 489 data = ({
490 490 'draw': draw,
491 491 'data': ssh_keys_data,
492 492 'recordsTotal': ssh_keys_data_total_count,
493 493 'recordsFiltered': users_data_total_filtered_count,
494 494 })
495 495
496 496 return data
497 497
498 498 @LoginRequired()
499 499 @HasPermissionAllDecorator('hg.admin')
500 500 @CSRFRequired()
501 501 @view_config(
502 502 route_name='admin_permissions_ssh_keys_update', request_method='POST',
503 503 renderer='rhodecode:templates/admin/permissions/permissions.mako')
504 504 def ssh_keys_update(self):
505 505 _ = self.request.translate
506 506 self.load_default_context()
507 507
508 508 ssh_enabled = self.ssh_enabled()
509 509 key_file = self.request.registry.settings.get(
510 510 'ssh.authorized_keys_file_path')
511 511 if ssh_enabled:
512 512 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
513 513 h.flash(_('Updated SSH keys file: {}').format(key_file),
514 514 category='success')
515 515 else:
516 516 h.flash(_('SSH key support is disabled in .ini file'),
517 517 category='warning')
518 518
519 519 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,325 +1,325 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 logging
22 22
23 23 from pyramid.view import view_config
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from packaging.version import Version
26 26
27 27 from rhodecode import events
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
33 33 HasRepoPermissionAny)
34 34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
35 35 from rhodecode.lib.utils2 import safe_int
36 36 from rhodecode.lib.vcs import RepositoryError
37 37 from rhodecode.model.db import Session, UserFollowing, User, Repository
38 38 from rhodecode.model.permission import PermissionModel
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.scm import ScmModel
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoSettingsView(RepoAppView):
46 46
47 47 def load_default_context(self):
48 48 c = self._get_local_tmpl_context()
49 49 return c
50 50
51 51 def _get_users_with_permissions(self):
52 52 user_permissions = {}
53 53 for perm in self.db_repo.permissions():
54 54 user_permissions[perm.user_id] = perm
55 55
56 56 return user_permissions
57 57
58 58 @LoginRequired()
59 59 @HasRepoPermissionAnyDecorator('repository.admin')
60 60 @view_config(
61 61 route_name='edit_repo_advanced', request_method='GET',
62 62 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
63 63 def edit_advanced(self):
64 64 _ = self.request.translate
65 65 c = self.load_default_context()
66 66 c.active = 'advanced'
67 67
68 c.default_user_id = User.get_default_user().user_id
68 c.default_user_id = User.get_default_user_id()
69 69 c.in_public_journal = UserFollowing.query() \
70 70 .filter(UserFollowing.user_id == c.default_user_id) \
71 71 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
72 72
73 73 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
74 74 c.hooks_outdated = False
75 75
76 76 try:
77 77 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
78 78 c.hooks_outdated = True
79 79 except Exception:
80 80 pass
81 81
82 82 # update commit cache if GET flag is present
83 83 if self.request.GET.get('update_commit_cache'):
84 84 self.db_repo.update_commit_cache()
85 85 h.flash(_('updated commit cache'), category='success')
86 86
87 87 return self._get_template_context(c)
88 88
89 89 @LoginRequired()
90 90 @HasRepoPermissionAnyDecorator('repository.admin')
91 91 @CSRFRequired()
92 92 @view_config(
93 93 route_name='edit_repo_advanced_archive', request_method='POST',
94 94 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
95 95 def edit_advanced_archive(self):
96 96 """
97 97 Archives the repository. It will become read-only, and not visible in search
98 98 or other queries. But still visible for super-admins.
99 99 """
100 100
101 101 _ = self.request.translate
102 102
103 103 try:
104 104 old_data = self.db_repo.get_api_data()
105 105 RepoModel().archive(self.db_repo)
106 106
107 107 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
108 108 audit_logger.store_web(
109 109 'repo.archive', action_data={'old_data': old_data},
110 110 user=self._rhodecode_user, repo=repo)
111 111
112 112 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
113 113 h.flash(
114 114 _('Archived repository `%s`') % self.db_repo_name,
115 115 category='success')
116 116 Session().commit()
117 117 except Exception:
118 118 log.exception("Exception during archiving of repository")
119 119 h.flash(_('An error occurred during archiving of `%s`')
120 120 % self.db_repo_name, category='error')
121 121 # redirect to advanced for more deletion options
122 122 raise HTTPFound(
123 123 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
124 124 _anchor='advanced-archive'))
125 125
126 126 # flush permissions for all users defined in permissions
127 127 affected_user_ids = self._get_users_with_permissions().keys()
128 128 PermissionModel().trigger_permission_flush(affected_user_ids)
129 129
130 130 raise HTTPFound(h.route_path('home'))
131 131
132 132 @LoginRequired()
133 133 @HasRepoPermissionAnyDecorator('repository.admin')
134 134 @CSRFRequired()
135 135 @view_config(
136 136 route_name='edit_repo_advanced_delete', request_method='POST',
137 137 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
138 138 def edit_advanced_delete(self):
139 139 """
140 140 Deletes the repository, or shows warnings if deletion is not possible
141 141 because of attached forks or other errors.
142 142 """
143 143 _ = self.request.translate
144 144 handle_forks = self.request.POST.get('forks', None)
145 145 if handle_forks == 'detach_forks':
146 146 handle_forks = 'detach'
147 147 elif handle_forks == 'delete_forks':
148 148 handle_forks = 'delete'
149 149
150 150 try:
151 151 old_data = self.db_repo.get_api_data()
152 152 RepoModel().delete(self.db_repo, forks=handle_forks)
153 153
154 154 _forks = self.db_repo.forks.count()
155 155 if _forks and handle_forks:
156 156 if handle_forks == 'detach_forks':
157 157 h.flash(_('Detached %s forks') % _forks, category='success')
158 158 elif handle_forks == 'delete_forks':
159 159 h.flash(_('Deleted %s forks') % _forks, category='success')
160 160
161 161 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
162 162 audit_logger.store_web(
163 163 'repo.delete', action_data={'old_data': old_data},
164 164 user=self._rhodecode_user, repo=repo)
165 165
166 166 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
167 167 h.flash(
168 168 _('Deleted repository `%s`') % self.db_repo_name,
169 169 category='success')
170 170 Session().commit()
171 171 except AttachedForksError:
172 172 repo_advanced_url = h.route_path(
173 173 'edit_repo_advanced', repo_name=self.db_repo_name,
174 174 _anchor='advanced-delete')
175 175 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
176 176 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
177 177 'Try using {delete_or_detach} option.')
178 178 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
179 179 category='warning')
180 180
181 181 # redirect to advanced for forks handle action ?
182 182 raise HTTPFound(repo_advanced_url)
183 183
184 184 except AttachedPullRequestsError:
185 185 repo_advanced_url = h.route_path(
186 186 'edit_repo_advanced', repo_name=self.db_repo_name,
187 187 _anchor='advanced-delete')
188 188 attached_prs = len(self.db_repo.pull_requests_source +
189 189 self.db_repo.pull_requests_target)
190 190 h.flash(
191 191 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
192 192 'Consider archiving the repository instead.').format(
193 193 repo=self.db_repo_name, num=attached_prs), category='warning')
194 194
195 195 # redirect to advanced for forks handle action ?
196 196 raise HTTPFound(repo_advanced_url)
197 197
198 198 except Exception:
199 199 log.exception("Exception during deletion of repository")
200 200 h.flash(_('An error occurred during deletion of `%s`')
201 201 % self.db_repo_name, category='error')
202 202 # redirect to advanced for more deletion options
203 203 raise HTTPFound(
204 204 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
205 205 _anchor='advanced-delete'))
206 206
207 207 raise HTTPFound(h.route_path('home'))
208 208
209 209 @LoginRequired()
210 210 @HasRepoPermissionAnyDecorator('repository.admin')
211 211 @CSRFRequired()
212 212 @view_config(
213 213 route_name='edit_repo_advanced_journal', request_method='POST',
214 214 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
215 215 def edit_advanced_journal(self):
216 216 """
217 217 Set's this repository to be visible in public journal,
218 218 in other words making default user to follow this repo
219 219 """
220 220 _ = self.request.translate
221 221
222 222 try:
223 user_id = User.get_default_user().user_id
223 user_id = User.get_default_user_id()
224 224 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
225 225 h.flash(_('Updated repository visibility in public journal'),
226 226 category='success')
227 227 Session().commit()
228 228 except Exception:
229 229 h.flash(_('An error occurred during setting this '
230 230 'repository in public journal'),
231 231 category='error')
232 232
233 233 raise HTTPFound(
234 234 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator('repository.admin')
238 238 @CSRFRequired()
239 239 @view_config(
240 240 route_name='edit_repo_advanced_fork', request_method='POST',
241 241 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
242 242 def edit_advanced_fork(self):
243 243 """
244 244 Mark given repository as a fork of another
245 245 """
246 246 _ = self.request.translate
247 247
248 248 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
249 249
250 250 # valid repo, re-check permissions
251 251 if new_fork_id:
252 252 repo = Repository.get(new_fork_id)
253 253 # ensure we have at least read access to the repo we mark
254 254 perm_check = HasRepoPermissionAny(
255 255 'repository.read', 'repository.write', 'repository.admin')
256 256
257 257 if repo and perm_check(repo_name=repo.repo_name):
258 258 new_fork_id = repo.repo_id
259 259 else:
260 260 new_fork_id = None
261 261
262 262 try:
263 263 repo = ScmModel().mark_as_fork(
264 264 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
265 265 fork = repo.fork.repo_name if repo.fork else _('Nothing')
266 266 Session().commit()
267 267 h.flash(
268 268 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
269 269 category='success')
270 270 except RepositoryError as e:
271 271 log.exception("Repository Error occurred")
272 272 h.flash(str(e), category='error')
273 273 except Exception:
274 274 log.exception("Exception while editing fork")
275 275 h.flash(_('An error occurred during this operation'),
276 276 category='error')
277 277
278 278 raise HTTPFound(
279 279 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
280 280
281 281 @LoginRequired()
282 282 @HasRepoPermissionAnyDecorator('repository.admin')
283 283 @CSRFRequired()
284 284 @view_config(
285 285 route_name='edit_repo_advanced_locking', request_method='POST',
286 286 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
287 287 def edit_advanced_locking(self):
288 288 """
289 289 Toggle locking of repository
290 290 """
291 291 _ = self.request.translate
292 292 set_lock = self.request.POST.get('set_lock')
293 293 set_unlock = self.request.POST.get('set_unlock')
294 294
295 295 try:
296 296 if set_lock:
297 297 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
298 298 lock_reason=Repository.LOCK_WEB)
299 299 h.flash(_('Locked repository'), category='success')
300 300 elif set_unlock:
301 301 Repository.unlock(self.db_repo)
302 302 h.flash(_('Unlocked repository'), category='success')
303 303 except Exception as e:
304 304 log.exception("Exception during unlocking")
305 305 h.flash(_('An error occurred during unlocking'), category='error')
306 306
307 307 raise HTTPFound(
308 308 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
309 309
310 310 @LoginRequired()
311 311 @HasRepoPermissionAnyDecorator('repository.admin')
312 312 @view_config(
313 313 route_name='edit_repo_advanced_hooks', request_method='GET',
314 314 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
315 315 def edit_advanced_install_hooks(self):
316 316 """
317 317 Install Hooks for repository
318 318 """
319 319 _ = self.request.translate
320 320 self.load_default_context()
321 321 self.rhodecode_vcs_repo.install_hooks(force=True)
322 322 h.flash(_('installed updated hooks into this repository'),
323 323 category='success')
324 324 raise HTTPFound(
325 325 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,86 +1,87 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 os
22 22 import logging
23 23 import rhodecode
24 24
25 25 from rhodecode.config import utils
26 26
27 27 from rhodecode.lib.utils import load_rcextensions
28 28 from rhodecode.lib.utils2 import str2bool
29 29 from rhodecode.lib.vcs import connect_vcs
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 def load_pyramid_environment(global_config, settings):
35 35 # Some parts of the code expect a merge of global and app settings.
36 36 settings_merged = global_config.copy()
37 37 settings_merged.update(settings)
38 38
39 39 # TODO(marcink): probably not required anymore
40 40 # configure channelstream,
41 41 settings_merged['channelstream_config'] = {
42 42 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
43 43 'server': settings_merged.get('channelstream.server'),
44 44 'secret': settings_merged.get('channelstream.secret')
45 45 }
46 46
47 47 # If this is a test run we prepare the test environment like
48 48 # creating a test database, test search index and test repositories.
49 49 # This has to be done before the database connection is initialized.
50 50 if settings['is_test']:
51 51 rhodecode.is_test = True
52 52 rhodecode.disable_error_handler = True
53 53 from rhodecode import authentication
54 54 authentication.plugin_default_auth_ttl = 0
55 55
56 56 utils.initialize_test_environment(settings_merged)
57 57
58 58 # Initialize the database connection.
59 59 utils.initialize_database(settings_merged)
60 60
61 61 load_rcextensions(root_path=settings_merged['here'])
62 62
63 63 # Limit backends to `vcs.backends` from configuration, and preserve the order
64 64 for alias in rhodecode.BACKENDS.keys():
65 65 if alias not in settings['vcs.backends']:
66 66 del rhodecode.BACKENDS[alias]
67 67
68 def sorter(item):
69 return settings['vcs.backends'].index(item[0])
70 rhodecode.BACKENDS = rhodecode.OrderedDict(sorted(rhodecode.BACKENDS.items(), key=sorter))
68 _sorted_backend = sorted(rhodecode.BACKENDS.items(),
69 key=lambda item: settings['vcs.backends'].index(item[0]))
70 rhodecode.BACKENDS = rhodecode.OrderedDict(_sorted_backend)
71 71
72 72 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
73 73
74 74 # initialize vcs client and optionally run the server if enabled
75 75 vcs_server_uri = settings['vcs.server']
76 76 vcs_server_enabled = settings['vcs.server.enable']
77 77
78 78 utils.configure_vcs(settings)
79 79
80 80 # Store the settings to make them available to other modules.
81 81
82 82 rhodecode.PYRAMID_SETTINGS = settings_merged
83 83 rhodecode.CONFIG = settings_merged
84 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
84 85
85 86 if vcs_server_enabled:
86 87 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
@@ -1,93 +1,101 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 os
22 import shlex
23 22 import platform
24 23
25 24 from rhodecode.model import init_model
26 25
27 26
28 27 def configure_vcs(config):
29 28 """
30 29 Patch VCS config with some RhodeCode specific stuff
31 30 """
32 31 from rhodecode.lib.vcs import conf
33 32 import rhodecode.lib.vcs.conf.settings
34 33
35 34 conf.settings.BACKENDS = {
36 35 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
37 36 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
38 37 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
39 38 }
40 39
41 40 conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol']
42 41 conf.settings.HOOKS_HOST = config['vcs.hooks.host']
43 42 conf.settings.HOOKS_DIRECT_CALLS = config['vcs.hooks.direct_calls']
44 43 conf.settings.DEFAULT_ENCODINGS = config['default_encoding']
45 44 conf.settings.ALIASES[:] = config['vcs.backends']
46 45 conf.settings.SVN_COMPATIBLE_VERSION = config['vcs.svn.compatible_version']
47 46
48 47
49 48 def initialize_database(config):
50 49 from rhodecode.lib.utils2 import engine_from_config, get_encryption_key
51 50 engine = engine_from_config(config, 'sqlalchemy.db1.')
52 51 init_model(engine, encryption_key=get_encryption_key(config))
53 52
54 53
55 54 def initialize_test_environment(settings, test_env=None):
56 55 if test_env is None:
57 56 test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0))
58 57
59 58 from rhodecode.lib.utils import (
60 59 create_test_directory, create_test_database, create_test_repositories,
61 60 create_test_index)
62 61 from rhodecode.tests import TESTS_TMP_PATH
63 62 from rhodecode.lib.vcs.backends.hg import largefiles_store
64 63 from rhodecode.lib.vcs.backends.git import lfs_store
65 64
66 65 # test repos
67 66 if test_env:
68 67 create_test_directory(TESTS_TMP_PATH)
69 68 # large object stores
70 69 create_test_directory(largefiles_store(TESTS_TMP_PATH))
71 70 create_test_directory(lfs_store(TESTS_TMP_PATH))
72 71
73 72 create_test_database(TESTS_TMP_PATH, settings)
74 73 create_test_repositories(TESTS_TMP_PATH, settings)
75 74 create_test_index(TESTS_TMP_PATH, settings)
76 75
77 76
78 77 def get_vcs_server_protocol(config):
79 78 return config['vcs.server.protocol']
80 79
81 80
82 81 def set_instance_id(config):
83 82 """
84 83 Sets a dynamic generated config['instance_id'] if missing or '*'
85 84 E.g instance_id = *cluster-1 or instance_id = *
86 85 """
87 86
88 87 config['instance_id'] = config.get('instance_id') or ''
89 88 instance_id = config['instance_id']
90 89 if instance_id.startswith('*') or not instance_id:
91 90 prefix = instance_id.lstrip('*')
92 91 _platform_id = platform.uname()[1] or 'instance'
93 92 config['instance_id'] = '%s%s-%s' % (prefix, _platform_id, os.getpid())
93
94
95 def get_default_user_id():
96 from rhodecode.model.db import User, Session
97 user_id = Session()\
98 .query(User.user_id)\
99 .filter(User.username == User.DEFAULT_USER)\
100 .scalar()
101 return user_id
@@ -1,5586 +1,5591 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, cast, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers2.text import remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 60 from rhodecode.lib.utils2 import (
61 61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 66 from rhodecode.lib.ext_json import json
67 67 from rhodecode.lib.caching_query import FromCache
68 68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY = None
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 return prefix + obj.username
107 107
108 108
109 109 def display_user_group_sort(obj):
110 110 """
111 111 Sort function used to sort permissions in .permissions() function of
112 112 Repository, RepoGroup, UserGroup. Also it put the default user in front
113 113 of all other resources
114 114 """
115 115
116 116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
117 117 return prefix + obj.users_group_name
118 118
119 119
120 120 def _hash_key(k):
121 121 return sha1_safe(k)
122 122
123 123
124 124 def in_filter_generator(qry, items, limit=500):
125 125 """
126 126 Splits IN() into multiple with OR
127 127 e.g.::
128 128 cnt = Repository.query().filter(
129 129 or_(
130 130 *in_filter_generator(Repository.repo_id, range(100000))
131 131 )).count()
132 132 """
133 133 if not items:
134 134 # empty list will cause empty query which might cause security issues
135 135 # this can lead to hidden unpleasant results
136 136 items = [-1]
137 137
138 138 parts = []
139 139 for chunk in xrange(0, len(items), limit):
140 140 parts.append(
141 141 qry.in_(items[chunk: chunk + limit])
142 142 )
143 143
144 144 return parts
145 145
146 146
147 147 base_table_args = {
148 148 'extend_existing': True,
149 149 'mysql_engine': 'InnoDB',
150 150 'mysql_charset': 'utf8',
151 151 'sqlite_autoincrement': True
152 152 }
153 153
154 154
155 155 class EncryptedTextValue(TypeDecorator):
156 156 """
157 157 Special column for encrypted long text data, use like::
158 158
159 159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
160 160
161 161 This column is intelligent so if value is in unencrypted form it return
162 162 unencrypted form, but on save it always encrypts
163 163 """
164 164 impl = Text
165 165
166 166 def process_bind_param(self, value, dialect):
167 167 """
168 168 Setter for storing value
169 169 """
170 170 import rhodecode
171 171 if not value:
172 172 return value
173 173
174 174 # protect against double encrypting if values is already encrypted
175 175 if value.startswith('enc$aes$') \
176 176 or value.startswith('enc$aes_hmac$') \
177 177 or value.startswith('enc2$'):
178 178 raise ValueError('value needs to be in unencrypted format, '
179 179 'ie. not starting with enc$ or enc2$')
180 180
181 181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
182 182 if algo == 'aes':
183 183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
184 184 elif algo == 'fernet':
185 185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
186 186 else:
187 187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
188 188
189 189 def process_result_value(self, value, dialect):
190 190 """
191 191 Getter for retrieving value
192 192 """
193 193
194 194 import rhodecode
195 195 if not value:
196 196 return value
197 197
198 198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
199 199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
200 200 if algo == 'aes':
201 201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
202 202 elif algo == 'fernet':
203 203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
204 204 else:
205 205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
206 206 return decrypted_data
207 207
208 208
209 209 class BaseModel(object):
210 210 """
211 211 Base Model for all classes
212 212 """
213 213
214 214 @classmethod
215 215 def _get_keys(cls):
216 216 """return column names for this model """
217 217 return class_mapper(cls).c.keys()
218 218
219 219 def get_dict(self):
220 220 """
221 221 return dict with keys and values corresponding
222 222 to this model data """
223 223
224 224 d = {}
225 225 for k in self._get_keys():
226 226 d[k] = getattr(self, k)
227 227
228 228 # also use __json__() if present to get additional fields
229 229 _json_attr = getattr(self, '__json__', None)
230 230 if _json_attr:
231 231 # update with attributes from __json__
232 232 if callable(_json_attr):
233 233 _json_attr = _json_attr()
234 234 for k, val in _json_attr.iteritems():
235 235 d[k] = val
236 236 return d
237 237
238 238 def get_appstruct(self):
239 239 """return list with keys and values tuples corresponding
240 240 to this model data """
241 241
242 242 lst = []
243 243 for k in self._get_keys():
244 244 lst.append((k, getattr(self, k),))
245 245 return lst
246 246
247 247 def populate_obj(self, populate_dict):
248 248 """populate model with data from given populate_dict"""
249 249
250 250 for k in self._get_keys():
251 251 if k in populate_dict:
252 252 setattr(self, k, populate_dict[k])
253 253
254 254 @classmethod
255 255 def query(cls):
256 256 return Session().query(cls)
257 257
258 258 @classmethod
259 259 def get(cls, id_):
260 260 if id_:
261 261 return cls.query().get(id_)
262 262
263 263 @classmethod
264 264 def get_or_404(cls, id_):
265 265 from pyramid.httpexceptions import HTTPNotFound
266 266
267 267 try:
268 268 id_ = int(id_)
269 269 except (TypeError, ValueError):
270 270 raise HTTPNotFound()
271 271
272 272 res = cls.query().get(id_)
273 273 if not res:
274 274 raise HTTPNotFound()
275 275 return res
276 276
277 277 @classmethod
278 278 def getAll(cls):
279 279 # deprecated and left for backward compatibility
280 280 return cls.get_all()
281 281
282 282 @classmethod
283 283 def get_all(cls):
284 284 return cls.query().all()
285 285
286 286 @classmethod
287 287 def delete(cls, id_):
288 288 obj = cls.query().get(id_)
289 289 Session().delete(obj)
290 290
291 291 @classmethod
292 292 def identity_cache(cls, session, attr_name, value):
293 293 exist_in_session = []
294 294 for (item_cls, pkey), instance in session.identity_map.items():
295 295 if cls == item_cls and getattr(instance, attr_name) == value:
296 296 exist_in_session.append(instance)
297 297 if exist_in_session:
298 298 if len(exist_in_session) == 1:
299 299 return exist_in_session[0]
300 300 log.exception(
301 301 'multiple objects with attr %s and '
302 302 'value %s found with same name: %r',
303 303 attr_name, value, exist_in_session)
304 304
305 305 def __repr__(self):
306 306 if hasattr(self, '__unicode__'):
307 307 # python repr needs to return str
308 308 try:
309 309 return safe_str(self.__unicode__())
310 310 except UnicodeDecodeError:
311 311 pass
312 312 return '<DB:%s>' % (self.__class__.__name__)
313 313
314 314
315 315 class RhodeCodeSetting(Base, BaseModel):
316 316 __tablename__ = 'rhodecode_settings'
317 317 __table_args__ = (
318 318 UniqueConstraint('app_settings_name'),
319 319 base_table_args
320 320 )
321 321
322 322 SETTINGS_TYPES = {
323 323 'str': safe_str,
324 324 'int': safe_int,
325 325 'unicode': safe_unicode,
326 326 'bool': str2bool,
327 327 'list': functools.partial(aslist, sep=',')
328 328 }
329 329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
330 330 GLOBAL_CONF_KEY = 'app_settings'
331 331
332 332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
333 333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
334 334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
335 335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
336 336
337 337 def __init__(self, key='', val='', type='unicode'):
338 338 self.app_settings_name = key
339 339 self.app_settings_type = type
340 340 self.app_settings_value = val
341 341
342 342 @validates('_app_settings_value')
343 343 def validate_settings_value(self, key, val):
344 344 assert type(val) == unicode
345 345 return val
346 346
347 347 @hybrid_property
348 348 def app_settings_value(self):
349 349 v = self._app_settings_value
350 350 _type = self.app_settings_type
351 351 if _type:
352 352 _type = self.app_settings_type.split('.')[0]
353 353 # decode the encrypted value
354 354 if 'encrypted' in self.app_settings_type:
355 355 cipher = EncryptedTextValue()
356 356 v = safe_unicode(cipher.process_result_value(v, None))
357 357
358 358 converter = self.SETTINGS_TYPES.get(_type) or \
359 359 self.SETTINGS_TYPES['unicode']
360 360 return converter(v)
361 361
362 362 @app_settings_value.setter
363 363 def app_settings_value(self, val):
364 364 """
365 365 Setter that will always make sure we use unicode in app_settings_value
366 366
367 367 :param val:
368 368 """
369 369 val = safe_unicode(val)
370 370 # encode the encrypted value
371 371 if 'encrypted' in self.app_settings_type:
372 372 cipher = EncryptedTextValue()
373 373 val = safe_unicode(cipher.process_bind_param(val, None))
374 374 self._app_settings_value = val
375 375
376 376 @hybrid_property
377 377 def app_settings_type(self):
378 378 return self._app_settings_type
379 379
380 380 @app_settings_type.setter
381 381 def app_settings_type(self, val):
382 382 if val.split('.')[0] not in self.SETTINGS_TYPES:
383 383 raise Exception('type must be one of %s got %s'
384 384 % (self.SETTINGS_TYPES.keys(), val))
385 385 self._app_settings_type = val
386 386
387 387 @classmethod
388 388 def get_by_prefix(cls, prefix):
389 389 return RhodeCodeSetting.query()\
390 390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
391 391 .all()
392 392
393 393 def __unicode__(self):
394 394 return u"<%s('%s:%s[%s]')>" % (
395 395 self.__class__.__name__,
396 396 self.app_settings_name, self.app_settings_value,
397 397 self.app_settings_type
398 398 )
399 399
400 400
401 401 class RhodeCodeUi(Base, BaseModel):
402 402 __tablename__ = 'rhodecode_ui'
403 403 __table_args__ = (
404 404 UniqueConstraint('ui_key'),
405 405 base_table_args
406 406 )
407 407
408 408 HOOK_REPO_SIZE = 'changegroup.repo_size'
409 409 # HG
410 410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
411 411 HOOK_PULL = 'outgoing.pull_logger'
412 412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
413 413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
414 414 HOOK_PUSH = 'changegroup.push_logger'
415 415 HOOK_PUSH_KEY = 'pushkey.key_push'
416 416
417 417 HOOKS_BUILTIN = [
418 418 HOOK_PRE_PULL,
419 419 HOOK_PULL,
420 420 HOOK_PRE_PUSH,
421 421 HOOK_PRETX_PUSH,
422 422 HOOK_PUSH,
423 423 HOOK_PUSH_KEY,
424 424 ]
425 425
426 426 # TODO: johbo: Unify way how hooks are configured for git and hg,
427 427 # git part is currently hardcoded.
428 428
429 429 # SVN PATTERNS
430 430 SVN_BRANCH_ID = 'vcs_svn_branch'
431 431 SVN_TAG_ID = 'vcs_svn_tag'
432 432
433 433 ui_id = Column(
434 434 "ui_id", Integer(), nullable=False, unique=True, default=None,
435 435 primary_key=True)
436 436 ui_section = Column(
437 437 "ui_section", String(255), nullable=True, unique=None, default=None)
438 438 ui_key = Column(
439 439 "ui_key", String(255), nullable=True, unique=None, default=None)
440 440 ui_value = Column(
441 441 "ui_value", String(255), nullable=True, unique=None, default=None)
442 442 ui_active = Column(
443 443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
444 444
445 445 def __repr__(self):
446 446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
447 447 self.ui_key, self.ui_value)
448 448
449 449
450 450 class RepoRhodeCodeSetting(Base, BaseModel):
451 451 __tablename__ = 'repo_rhodecode_settings'
452 452 __table_args__ = (
453 453 UniqueConstraint(
454 454 'app_settings_name', 'repository_id',
455 455 name='uq_repo_rhodecode_setting_name_repo_id'),
456 456 base_table_args
457 457 )
458 458
459 459 repository_id = Column(
460 460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
461 461 nullable=False)
462 462 app_settings_id = Column(
463 463 "app_settings_id", Integer(), nullable=False, unique=True,
464 464 default=None, primary_key=True)
465 465 app_settings_name = Column(
466 466 "app_settings_name", String(255), nullable=True, unique=None,
467 467 default=None)
468 468 _app_settings_value = Column(
469 469 "app_settings_value", String(4096), nullable=True, unique=None,
470 470 default=None)
471 471 _app_settings_type = Column(
472 472 "app_settings_type", String(255), nullable=True, unique=None,
473 473 default=None)
474 474
475 475 repository = relationship('Repository')
476 476
477 477 def __init__(self, repository_id, key='', val='', type='unicode'):
478 478 self.repository_id = repository_id
479 479 self.app_settings_name = key
480 480 self.app_settings_type = type
481 481 self.app_settings_value = val
482 482
483 483 @validates('_app_settings_value')
484 484 def validate_settings_value(self, key, val):
485 485 assert type(val) == unicode
486 486 return val
487 487
488 488 @hybrid_property
489 489 def app_settings_value(self):
490 490 v = self._app_settings_value
491 491 type_ = self.app_settings_type
492 492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
493 493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
494 494 return converter(v)
495 495
496 496 @app_settings_value.setter
497 497 def app_settings_value(self, val):
498 498 """
499 499 Setter that will always make sure we use unicode in app_settings_value
500 500
501 501 :param val:
502 502 """
503 503 self._app_settings_value = safe_unicode(val)
504 504
505 505 @hybrid_property
506 506 def app_settings_type(self):
507 507 return self._app_settings_type
508 508
509 509 @app_settings_type.setter
510 510 def app_settings_type(self, val):
511 511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 512 if val not in SETTINGS_TYPES:
513 513 raise Exception('type must be one of %s got %s'
514 514 % (SETTINGS_TYPES.keys(), val))
515 515 self._app_settings_type = val
516 516
517 517 def __unicode__(self):
518 518 return u"<%s('%s:%s:%s[%s]')>" % (
519 519 self.__class__.__name__, self.repository.repo_name,
520 520 self.app_settings_name, self.app_settings_value,
521 521 self.app_settings_type
522 522 )
523 523
524 524
525 525 class RepoRhodeCodeUi(Base, BaseModel):
526 526 __tablename__ = 'repo_rhodecode_ui'
527 527 __table_args__ = (
528 528 UniqueConstraint(
529 529 'repository_id', 'ui_section', 'ui_key',
530 530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
531 531 base_table_args
532 532 )
533 533
534 534 repository_id = Column(
535 535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
536 536 nullable=False)
537 537 ui_id = Column(
538 538 "ui_id", Integer(), nullable=False, unique=True, default=None,
539 539 primary_key=True)
540 540 ui_section = Column(
541 541 "ui_section", String(255), nullable=True, unique=None, default=None)
542 542 ui_key = Column(
543 543 "ui_key", String(255), nullable=True, unique=None, default=None)
544 544 ui_value = Column(
545 545 "ui_value", String(255), nullable=True, unique=None, default=None)
546 546 ui_active = Column(
547 547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
548 548
549 549 repository = relationship('Repository')
550 550
551 551 def __repr__(self):
552 552 return '<%s[%s:%s]%s=>%s]>' % (
553 553 self.__class__.__name__, self.repository.repo_name,
554 554 self.ui_section, self.ui_key, self.ui_value)
555 555
556 556
557 557 class User(Base, BaseModel):
558 558 __tablename__ = 'users'
559 559 __table_args__ = (
560 560 UniqueConstraint('username'), UniqueConstraint('email'),
561 561 Index('u_username_idx', 'username'),
562 562 Index('u_email_idx', 'email'),
563 563 base_table_args
564 564 )
565 565
566 566 DEFAULT_USER = 'default'
567 567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
568 568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
569 569
570 570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 571 username = Column("username", String(255), nullable=True, unique=None, default=None)
572 572 password = Column("password", String(255), nullable=True, unique=None, default=None)
573 573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
574 574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
575 575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
576 576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
577 577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
578 578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
579 579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
580 580 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
581 581
582 582 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
583 583 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
584 584 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
585 585 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
586 586 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
587 587 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
588 588
589 589 user_log = relationship('UserLog')
590 590 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
591 591
592 592 repositories = relationship('Repository')
593 593 repository_groups = relationship('RepoGroup')
594 594 user_groups = relationship('UserGroup')
595 595
596 596 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
597 597 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
598 598
599 599 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 600 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 601 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
602 602
603 603 group_member = relationship('UserGroupMember', cascade='all')
604 604
605 605 notifications = relationship('UserNotification', cascade='all')
606 606 # notifications assigned to this user
607 607 user_created_notifications = relationship('Notification', cascade='all')
608 608 # comments created by this user
609 609 user_comments = relationship('ChangesetComment', cascade='all')
610 610 # user profile extra info
611 611 user_emails = relationship('UserEmailMap', cascade='all')
612 612 user_ip_map = relationship('UserIpMap', cascade='all')
613 613 user_auth_tokens = relationship('UserApiKeys', cascade='all')
614 614 user_ssh_keys = relationship('UserSshKeys', cascade='all')
615 615
616 616 # gists
617 617 user_gists = relationship('Gist', cascade='all')
618 618 # user pull requests
619 619 user_pull_requests = relationship('PullRequest', cascade='all')
620 620 # external identities
621 621 external_identities = relationship(
622 622 'ExternalIdentity',
623 623 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
624 624 cascade='all')
625 625 # review rules
626 626 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
627 627
628 628 # artifacts owned
629 629 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
630 630
631 631 # no cascade, set NULL
632 632 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
633 633
634 634 def __unicode__(self):
635 635 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
636 636 self.user_id, self.username)
637 637
638 638 @hybrid_property
639 639 def email(self):
640 640 return self._email
641 641
642 642 @email.setter
643 643 def email(self, val):
644 644 self._email = val.lower() if val else None
645 645
646 646 @hybrid_property
647 647 def first_name(self):
648 648 from rhodecode.lib import helpers as h
649 649 if self.name:
650 650 return h.escape(self.name)
651 651 return self.name
652 652
653 653 @hybrid_property
654 654 def last_name(self):
655 655 from rhodecode.lib import helpers as h
656 656 if self.lastname:
657 657 return h.escape(self.lastname)
658 658 return self.lastname
659 659
660 660 @hybrid_property
661 661 def api_key(self):
662 662 """
663 663 Fetch if exist an auth-token with role ALL connected to this user
664 664 """
665 665 user_auth_token = UserApiKeys.query()\
666 666 .filter(UserApiKeys.user_id == self.user_id)\
667 667 .filter(or_(UserApiKeys.expires == -1,
668 668 UserApiKeys.expires >= time.time()))\
669 669 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
670 670 if user_auth_token:
671 671 user_auth_token = user_auth_token.api_key
672 672
673 673 return user_auth_token
674 674
675 675 @api_key.setter
676 676 def api_key(self, val):
677 677 # don't allow to set API key this is deprecated for now
678 678 self._api_key = None
679 679
680 680 @property
681 681 def reviewer_pull_requests(self):
682 682 return PullRequestReviewers.query() \
683 683 .options(joinedload(PullRequestReviewers.pull_request)) \
684 684 .filter(PullRequestReviewers.user_id == self.user_id) \
685 685 .all()
686 686
687 687 @property
688 688 def firstname(self):
689 689 # alias for future
690 690 return self.name
691 691
692 692 @property
693 693 def emails(self):
694 694 other = UserEmailMap.query()\
695 695 .filter(UserEmailMap.user == self) \
696 696 .order_by(UserEmailMap.email_id.asc()) \
697 697 .all()
698 698 return [self.email] + [x.email for x in other]
699 699
700 700 def emails_cached(self):
701 701 emails = UserEmailMap.query()\
702 702 .filter(UserEmailMap.user == self) \
703 703 .order_by(UserEmailMap.email_id.asc())
704 704
705 705 emails = emails.options(
706 706 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
707 707 )
708 708
709 709 return [self.email] + [x.email for x in emails]
710 710
711 711 @property
712 712 def auth_tokens(self):
713 713 auth_tokens = self.get_auth_tokens()
714 714 return [x.api_key for x in auth_tokens]
715 715
716 716 def get_auth_tokens(self):
717 717 return UserApiKeys.query()\
718 718 .filter(UserApiKeys.user == self)\
719 719 .order_by(UserApiKeys.user_api_key_id.asc())\
720 720 .all()
721 721
722 722 @LazyProperty
723 723 def feed_token(self):
724 724 return self.get_feed_token()
725 725
726 726 def get_feed_token(self, cache=True):
727 727 feed_tokens = UserApiKeys.query()\
728 728 .filter(UserApiKeys.user == self)\
729 729 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
730 730 if cache:
731 731 feed_tokens = feed_tokens.options(
732 732 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
733 733
734 734 feed_tokens = feed_tokens.all()
735 735 if feed_tokens:
736 736 return feed_tokens[0].api_key
737 737 return 'NO_FEED_TOKEN_AVAILABLE'
738 738
739 739 @LazyProperty
740 740 def artifact_token(self):
741 741 return self.get_artifact_token()
742 742
743 743 def get_artifact_token(self, cache=True):
744 744 artifacts_tokens = UserApiKeys.query()\
745 745 .filter(UserApiKeys.user == self)\
746 746 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
747 747 if cache:
748 748 artifacts_tokens = artifacts_tokens.options(
749 749 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
750 750
751 751 artifacts_tokens = artifacts_tokens.all()
752 752 if artifacts_tokens:
753 753 return artifacts_tokens[0].api_key
754 754 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
755 755
756 756 @classmethod
757 757 def get(cls, user_id, cache=False):
758 758 if not user_id:
759 759 return
760 760
761 761 user = cls.query()
762 762 if cache:
763 763 user = user.options(
764 764 FromCache("sql_cache_short", "get_users_%s" % user_id))
765 765 return user.get(user_id)
766 766
767 767 @classmethod
768 768 def extra_valid_auth_tokens(cls, user, role=None):
769 769 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
770 770 .filter(or_(UserApiKeys.expires == -1,
771 771 UserApiKeys.expires >= time.time()))
772 772 if role:
773 773 tokens = tokens.filter(or_(UserApiKeys.role == role,
774 774 UserApiKeys.role == UserApiKeys.ROLE_ALL))
775 775 return tokens.all()
776 776
777 777 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
778 778 from rhodecode.lib import auth
779 779
780 780 log.debug('Trying to authenticate user: %s via auth-token, '
781 781 'and roles: %s', self, roles)
782 782
783 783 if not auth_token:
784 784 return False
785 785
786 786 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
787 787 tokens_q = UserApiKeys.query()\
788 788 .filter(UserApiKeys.user_id == self.user_id)\
789 789 .filter(or_(UserApiKeys.expires == -1,
790 790 UserApiKeys.expires >= time.time()))
791 791
792 792 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
793 793
794 794 crypto_backend = auth.crypto_backend()
795 795 enc_token_map = {}
796 796 plain_token_map = {}
797 797 for token in tokens_q:
798 798 if token.api_key.startswith(crypto_backend.ENC_PREF):
799 799 enc_token_map[token.api_key] = token
800 800 else:
801 801 plain_token_map[token.api_key] = token
802 802 log.debug(
803 803 'Found %s plain and %s encrypted tokens to check for authentication for this user',
804 804 len(plain_token_map), len(enc_token_map))
805 805
806 806 # plain token match comes first
807 807 match = plain_token_map.get(auth_token)
808 808
809 809 # check encrypted tokens now
810 810 if not match:
811 811 for token_hash, token in enc_token_map.items():
812 812 # NOTE(marcink): this is expensive to calculate, but most secure
813 813 if crypto_backend.hash_check(auth_token, token_hash):
814 814 match = token
815 815 break
816 816
817 817 if match:
818 818 log.debug('Found matching token %s', match)
819 819 if match.repo_id:
820 820 log.debug('Found scope, checking for scope match of token %s', match)
821 821 if match.repo_id == scope_repo_id:
822 822 return True
823 823 else:
824 824 log.debug(
825 825 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
826 826 'and calling scope is:%s, skipping further checks',
827 827 match.repo, scope_repo_id)
828 828 return False
829 829 else:
830 830 return True
831 831
832 832 return False
833 833
834 834 @property
835 835 def ip_addresses(self):
836 836 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
837 837 return [x.ip_addr for x in ret]
838 838
839 839 @property
840 840 def username_and_name(self):
841 841 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
842 842
843 843 @property
844 844 def username_or_name_or_email(self):
845 845 full_name = self.full_name if self.full_name is not ' ' else None
846 846 return self.username or full_name or self.email
847 847
848 848 @property
849 849 def full_name(self):
850 850 return '%s %s' % (self.first_name, self.last_name)
851 851
852 852 @property
853 853 def full_name_or_username(self):
854 854 return ('%s %s' % (self.first_name, self.last_name)
855 855 if (self.first_name and self.last_name) else self.username)
856 856
857 857 @property
858 858 def full_contact(self):
859 859 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
860 860
861 861 @property
862 862 def short_contact(self):
863 863 return '%s %s' % (self.first_name, self.last_name)
864 864
865 865 @property
866 866 def is_admin(self):
867 867 return self.admin
868 868
869 869 @property
870 870 def language(self):
871 871 return self.user_data.get('language')
872 872
873 873 def AuthUser(self, **kwargs):
874 874 """
875 875 Returns instance of AuthUser for this user
876 876 """
877 877 from rhodecode.lib.auth import AuthUser
878 878 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
879 879
880 880 @hybrid_property
881 881 def user_data(self):
882 882 if not self._user_data:
883 883 return {}
884 884
885 885 try:
886 886 return json.loads(self._user_data)
887 887 except TypeError:
888 888 return {}
889 889
890 890 @user_data.setter
891 891 def user_data(self, val):
892 892 if not isinstance(val, dict):
893 893 raise Exception('user_data must be dict, got %s' % type(val))
894 894 try:
895 895 self._user_data = json.dumps(val)
896 896 except Exception:
897 897 log.error(traceback.format_exc())
898 898
899 899 @classmethod
900 900 def get_by_username(cls, username, case_insensitive=False,
901 901 cache=False, identity_cache=False):
902 902 session = Session()
903 903
904 904 if case_insensitive:
905 905 q = cls.query().filter(
906 906 func.lower(cls.username) == func.lower(username))
907 907 else:
908 908 q = cls.query().filter(cls.username == username)
909 909
910 910 if cache:
911 911 if identity_cache:
912 912 val = cls.identity_cache(session, 'username', username)
913 913 if val:
914 914 return val
915 915 else:
916 916 cache_key = "get_user_by_name_%s" % _hash_key(username)
917 917 q = q.options(
918 918 FromCache("sql_cache_short", cache_key))
919 919
920 920 return q.scalar()
921 921
922 922 @classmethod
923 923 def get_by_auth_token(cls, auth_token, cache=False):
924 924 q = UserApiKeys.query()\
925 925 .filter(UserApiKeys.api_key == auth_token)\
926 926 .filter(or_(UserApiKeys.expires == -1,
927 927 UserApiKeys.expires >= time.time()))
928 928 if cache:
929 929 q = q.options(
930 930 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
931 931
932 932 match = q.first()
933 933 if match:
934 934 return match.user
935 935
936 936 @classmethod
937 937 def get_by_email(cls, email, case_insensitive=False, cache=False):
938 938
939 939 if case_insensitive:
940 940 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
941 941
942 942 else:
943 943 q = cls.query().filter(cls.email == email)
944 944
945 945 email_key = _hash_key(email)
946 946 if cache:
947 947 q = q.options(
948 948 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
949 949
950 950 ret = q.scalar()
951 951 if ret is None:
952 952 q = UserEmailMap.query()
953 953 # try fetching in alternate email map
954 954 if case_insensitive:
955 955 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
956 956 else:
957 957 q = q.filter(UserEmailMap.email == email)
958 958 q = q.options(joinedload(UserEmailMap.user))
959 959 if cache:
960 960 q = q.options(
961 961 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
962 962 ret = getattr(q.scalar(), 'user', None)
963 963
964 964 return ret
965 965
966 966 @classmethod
967 967 def get_from_cs_author(cls, author):
968 968 """
969 969 Tries to get User objects out of commit author string
970 970
971 971 :param author:
972 972 """
973 973 from rhodecode.lib.helpers import email, author_name
974 974 # Valid email in the attribute passed, see if they're in the system
975 975 _email = email(author)
976 976 if _email:
977 977 user = cls.get_by_email(_email, case_insensitive=True)
978 978 if user:
979 979 return user
980 980 # Maybe we can match by username?
981 981 _author = author_name(author)
982 982 user = cls.get_by_username(_author, case_insensitive=True)
983 983 if user:
984 984 return user
985 985
986 986 def update_userdata(self, **kwargs):
987 987 usr = self
988 988 old = usr.user_data
989 989 old.update(**kwargs)
990 990 usr.user_data = old
991 991 Session().add(usr)
992 992 log.debug('updated userdata with %s', kwargs)
993 993
994 994 def update_lastlogin(self):
995 995 """Update user lastlogin"""
996 996 self.last_login = datetime.datetime.now()
997 997 Session().add(self)
998 998 log.debug('updated user %s lastlogin', self.username)
999 999
1000 1000 def update_password(self, new_password):
1001 1001 from rhodecode.lib.auth import get_crypt_password
1002 1002
1003 1003 self.password = get_crypt_password(new_password)
1004 1004 Session().add(self)
1005 1005
1006 1006 @classmethod
1007 1007 def get_first_super_admin(cls):
1008 1008 user = User.query()\
1009 1009 .filter(User.admin == true()) \
1010 1010 .order_by(User.user_id.asc()) \
1011 1011 .first()
1012 1012
1013 1013 if user is None:
1014 1014 raise Exception('FATAL: Missing administrative account!')
1015 1015 return user
1016 1016
1017 1017 @classmethod
1018 1018 def get_all_super_admins(cls, only_active=False):
1019 1019 """
1020 1020 Returns all admin accounts sorted by username
1021 1021 """
1022 1022 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1023 1023 if only_active:
1024 1024 qry = qry.filter(User.active == true())
1025 1025 return qry.all()
1026 1026
1027 1027 @classmethod
1028 1028 def get_all_user_ids(cls, only_active=True):
1029 1029 """
1030 1030 Returns all users IDs
1031 1031 """
1032 1032 qry = Session().query(User.user_id)
1033 1033
1034 1034 if only_active:
1035 1035 qry = qry.filter(User.active == true())
1036 1036 return [x.user_id for x in qry]
1037 1037
1038 1038 @classmethod
1039 1039 def get_default_user(cls, cache=False, refresh=False):
1040 1040 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1041 1041 if user is None:
1042 1042 raise Exception('FATAL: Missing default account!')
1043 1043 if refresh:
1044 1044 # The default user might be based on outdated state which
1045 1045 # has been loaded from the cache.
1046 1046 # A call to refresh() ensures that the
1047 1047 # latest state from the database is used.
1048 1048 Session().refresh(user)
1049 1049 return user
1050 1050
1051 @classmethod
1052 def get_default_user_id(cls):
1053 import rhodecode
1054 return rhodecode.CONFIG['default_user_id']
1055
1051 1056 def _get_default_perms(self, user, suffix=''):
1052 1057 from rhodecode.model.permission import PermissionModel
1053 1058 return PermissionModel().get_default_perms(user.user_perms, suffix)
1054 1059
1055 1060 def get_default_perms(self, suffix=''):
1056 1061 return self._get_default_perms(self, suffix)
1057 1062
1058 1063 def get_api_data(self, include_secrets=False, details='full'):
1059 1064 """
1060 1065 Common function for generating user related data for API
1061 1066
1062 1067 :param include_secrets: By default secrets in the API data will be replaced
1063 1068 by a placeholder value to prevent exposing this data by accident. In case
1064 1069 this data shall be exposed, set this flag to ``True``.
1065 1070
1066 1071 :param details: details can be 'basic|full' basic gives only a subset of
1067 1072 the available user information that includes user_id, name and emails.
1068 1073 """
1069 1074 user = self
1070 1075 user_data = self.user_data
1071 1076 data = {
1072 1077 'user_id': user.user_id,
1073 1078 'username': user.username,
1074 1079 'firstname': user.name,
1075 1080 'lastname': user.lastname,
1076 1081 'description': user.description,
1077 1082 'email': user.email,
1078 1083 'emails': user.emails,
1079 1084 }
1080 1085 if details == 'basic':
1081 1086 return data
1082 1087
1083 1088 auth_token_length = 40
1084 1089 auth_token_replacement = '*' * auth_token_length
1085 1090
1086 1091 extras = {
1087 1092 'auth_tokens': [auth_token_replacement],
1088 1093 'active': user.active,
1089 1094 'admin': user.admin,
1090 1095 'extern_type': user.extern_type,
1091 1096 'extern_name': user.extern_name,
1092 1097 'last_login': user.last_login,
1093 1098 'last_activity': user.last_activity,
1094 1099 'ip_addresses': user.ip_addresses,
1095 1100 'language': user_data.get('language')
1096 1101 }
1097 1102 data.update(extras)
1098 1103
1099 1104 if include_secrets:
1100 1105 data['auth_tokens'] = user.auth_tokens
1101 1106 return data
1102 1107
1103 1108 def __json__(self):
1104 1109 data = {
1105 1110 'full_name': self.full_name,
1106 1111 'full_name_or_username': self.full_name_or_username,
1107 1112 'short_contact': self.short_contact,
1108 1113 'full_contact': self.full_contact,
1109 1114 }
1110 1115 data.update(self.get_api_data())
1111 1116 return data
1112 1117
1113 1118
1114 1119 class UserApiKeys(Base, BaseModel):
1115 1120 __tablename__ = 'user_api_keys'
1116 1121 __table_args__ = (
1117 1122 Index('uak_api_key_idx', 'api_key'),
1118 1123 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1119 1124 base_table_args
1120 1125 )
1121 1126 __mapper_args__ = {}
1122 1127
1123 1128 # ApiKey role
1124 1129 ROLE_ALL = 'token_role_all'
1125 1130 ROLE_HTTP = 'token_role_http'
1126 1131 ROLE_VCS = 'token_role_vcs'
1127 1132 ROLE_API = 'token_role_api'
1128 1133 ROLE_FEED = 'token_role_feed'
1129 1134 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1130 1135 ROLE_PASSWORD_RESET = 'token_password_reset'
1131 1136
1132 1137 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1133 1138
1134 1139 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1135 1140 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1136 1141 api_key = Column("api_key", String(255), nullable=False, unique=True)
1137 1142 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1138 1143 expires = Column('expires', Float(53), nullable=False)
1139 1144 role = Column('role', String(255), nullable=True)
1140 1145 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1141 1146
1142 1147 # scope columns
1143 1148 repo_id = Column(
1144 1149 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1145 1150 nullable=True, unique=None, default=None)
1146 1151 repo = relationship('Repository', lazy='joined')
1147 1152
1148 1153 repo_group_id = Column(
1149 1154 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1150 1155 nullable=True, unique=None, default=None)
1151 1156 repo_group = relationship('RepoGroup', lazy='joined')
1152 1157
1153 1158 user = relationship('User', lazy='joined')
1154 1159
1155 1160 def __unicode__(self):
1156 1161 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1157 1162
1158 1163 def __json__(self):
1159 1164 data = {
1160 1165 'auth_token': self.api_key,
1161 1166 'role': self.role,
1162 1167 'scope': self.scope_humanized,
1163 1168 'expired': self.expired
1164 1169 }
1165 1170 return data
1166 1171
1167 1172 def get_api_data(self, include_secrets=False):
1168 1173 data = self.__json__()
1169 1174 if include_secrets:
1170 1175 return data
1171 1176 else:
1172 1177 data['auth_token'] = self.token_obfuscated
1173 1178 return data
1174 1179
1175 1180 @hybrid_property
1176 1181 def description_safe(self):
1177 1182 from rhodecode.lib import helpers as h
1178 1183 return h.escape(self.description)
1179 1184
1180 1185 @property
1181 1186 def expired(self):
1182 1187 if self.expires == -1:
1183 1188 return False
1184 1189 return time.time() > self.expires
1185 1190
1186 1191 @classmethod
1187 1192 def _get_role_name(cls, role):
1188 1193 return {
1189 1194 cls.ROLE_ALL: _('all'),
1190 1195 cls.ROLE_HTTP: _('http/web interface'),
1191 1196 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1192 1197 cls.ROLE_API: _('api calls'),
1193 1198 cls.ROLE_FEED: _('feed access'),
1194 1199 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1195 1200 }.get(role, role)
1196 1201
1197 1202 @property
1198 1203 def role_humanized(self):
1199 1204 return self._get_role_name(self.role)
1200 1205
1201 1206 def _get_scope(self):
1202 1207 if self.repo:
1203 1208 return 'Repository: {}'.format(self.repo.repo_name)
1204 1209 if self.repo_group:
1205 1210 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1206 1211 return 'Global'
1207 1212
1208 1213 @property
1209 1214 def scope_humanized(self):
1210 1215 return self._get_scope()
1211 1216
1212 1217 @property
1213 1218 def token_obfuscated(self):
1214 1219 if self.api_key:
1215 1220 return self.api_key[:4] + "****"
1216 1221
1217 1222
1218 1223 class UserEmailMap(Base, BaseModel):
1219 1224 __tablename__ = 'user_email_map'
1220 1225 __table_args__ = (
1221 1226 Index('uem_email_idx', 'email'),
1222 1227 UniqueConstraint('email'),
1223 1228 base_table_args
1224 1229 )
1225 1230 __mapper_args__ = {}
1226 1231
1227 1232 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1228 1233 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1229 1234 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1230 1235 user = relationship('User', lazy='joined')
1231 1236
1232 1237 @validates('_email')
1233 1238 def validate_email(self, key, email):
1234 1239 # check if this email is not main one
1235 1240 main_email = Session().query(User).filter(User.email == email).scalar()
1236 1241 if main_email is not None:
1237 1242 raise AttributeError('email %s is present is user table' % email)
1238 1243 return email
1239 1244
1240 1245 @hybrid_property
1241 1246 def email(self):
1242 1247 return self._email
1243 1248
1244 1249 @email.setter
1245 1250 def email(self, val):
1246 1251 self._email = val.lower() if val else None
1247 1252
1248 1253
1249 1254 class UserIpMap(Base, BaseModel):
1250 1255 __tablename__ = 'user_ip_map'
1251 1256 __table_args__ = (
1252 1257 UniqueConstraint('user_id', 'ip_addr'),
1253 1258 base_table_args
1254 1259 )
1255 1260 __mapper_args__ = {}
1256 1261
1257 1262 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1258 1263 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1259 1264 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1260 1265 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1261 1266 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1262 1267 user = relationship('User', lazy='joined')
1263 1268
1264 1269 @hybrid_property
1265 1270 def description_safe(self):
1266 1271 from rhodecode.lib import helpers as h
1267 1272 return h.escape(self.description)
1268 1273
1269 1274 @classmethod
1270 1275 def _get_ip_range(cls, ip_addr):
1271 1276 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1272 1277 return [str(net.network_address), str(net.broadcast_address)]
1273 1278
1274 1279 def __json__(self):
1275 1280 return {
1276 1281 'ip_addr': self.ip_addr,
1277 1282 'ip_range': self._get_ip_range(self.ip_addr),
1278 1283 }
1279 1284
1280 1285 def __unicode__(self):
1281 1286 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1282 1287 self.user_id, self.ip_addr)
1283 1288
1284 1289
1285 1290 class UserSshKeys(Base, BaseModel):
1286 1291 __tablename__ = 'user_ssh_keys'
1287 1292 __table_args__ = (
1288 1293 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1289 1294
1290 1295 UniqueConstraint('ssh_key_fingerprint'),
1291 1296
1292 1297 base_table_args
1293 1298 )
1294 1299 __mapper_args__ = {}
1295 1300
1296 1301 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1297 1302 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1298 1303 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1299 1304
1300 1305 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1301 1306
1302 1307 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1303 1308 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1304 1309 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1305 1310
1306 1311 user = relationship('User', lazy='joined')
1307 1312
1308 1313 def __json__(self):
1309 1314 data = {
1310 1315 'ssh_fingerprint': self.ssh_key_fingerprint,
1311 1316 'description': self.description,
1312 1317 'created_on': self.created_on
1313 1318 }
1314 1319 return data
1315 1320
1316 1321 def get_api_data(self):
1317 1322 data = self.__json__()
1318 1323 return data
1319 1324
1320 1325
1321 1326 class UserLog(Base, BaseModel):
1322 1327 __tablename__ = 'user_logs'
1323 1328 __table_args__ = (
1324 1329 base_table_args,
1325 1330 )
1326 1331
1327 1332 VERSION_1 = 'v1'
1328 1333 VERSION_2 = 'v2'
1329 1334 VERSIONS = [VERSION_1, VERSION_2]
1330 1335
1331 1336 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1332 1337 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1333 1338 username = Column("username", String(255), nullable=True, unique=None, default=None)
1334 1339 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1335 1340 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1336 1341 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1337 1342 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1338 1343 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1339 1344
1340 1345 version = Column("version", String(255), nullable=True, default=VERSION_1)
1341 1346 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1342 1347 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1343 1348
1344 1349 def __unicode__(self):
1345 1350 return u"<%s('id:%s:%s')>" % (
1346 1351 self.__class__.__name__, self.repository_name, self.action)
1347 1352
1348 1353 def __json__(self):
1349 1354 return {
1350 1355 'user_id': self.user_id,
1351 1356 'username': self.username,
1352 1357 'repository_id': self.repository_id,
1353 1358 'repository_name': self.repository_name,
1354 1359 'user_ip': self.user_ip,
1355 1360 'action_date': self.action_date,
1356 1361 'action': self.action,
1357 1362 }
1358 1363
1359 1364 @hybrid_property
1360 1365 def entry_id(self):
1361 1366 return self.user_log_id
1362 1367
1363 1368 @property
1364 1369 def action_as_day(self):
1365 1370 return datetime.date(*self.action_date.timetuple()[:3])
1366 1371
1367 1372 user = relationship('User')
1368 1373 repository = relationship('Repository', cascade='')
1369 1374
1370 1375
1371 1376 class UserGroup(Base, BaseModel):
1372 1377 __tablename__ = 'users_groups'
1373 1378 __table_args__ = (
1374 1379 base_table_args,
1375 1380 )
1376 1381
1377 1382 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1378 1383 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1379 1384 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1380 1385 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1381 1386 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1382 1387 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1383 1388 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1384 1389 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1385 1390
1386 1391 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1387 1392 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1388 1393 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1389 1394 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1390 1395 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1391 1396 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1392 1397
1393 1398 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1394 1399 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1395 1400
1396 1401 @classmethod
1397 1402 def _load_group_data(cls, column):
1398 1403 if not column:
1399 1404 return {}
1400 1405
1401 1406 try:
1402 1407 return json.loads(column) or {}
1403 1408 except TypeError:
1404 1409 return {}
1405 1410
1406 1411 @hybrid_property
1407 1412 def description_safe(self):
1408 1413 from rhodecode.lib import helpers as h
1409 1414 return h.escape(self.user_group_description)
1410 1415
1411 1416 @hybrid_property
1412 1417 def group_data(self):
1413 1418 return self._load_group_data(self._group_data)
1414 1419
1415 1420 @group_data.expression
1416 1421 def group_data(self, **kwargs):
1417 1422 return self._group_data
1418 1423
1419 1424 @group_data.setter
1420 1425 def group_data(self, val):
1421 1426 try:
1422 1427 self._group_data = json.dumps(val)
1423 1428 except Exception:
1424 1429 log.error(traceback.format_exc())
1425 1430
1426 1431 @classmethod
1427 1432 def _load_sync(cls, group_data):
1428 1433 if group_data:
1429 1434 return group_data.get('extern_type')
1430 1435
1431 1436 @property
1432 1437 def sync(self):
1433 1438 return self._load_sync(self.group_data)
1434 1439
1435 1440 def __unicode__(self):
1436 1441 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1437 1442 self.users_group_id,
1438 1443 self.users_group_name)
1439 1444
1440 1445 @classmethod
1441 1446 def get_by_group_name(cls, group_name, cache=False,
1442 1447 case_insensitive=False):
1443 1448 if case_insensitive:
1444 1449 q = cls.query().filter(func.lower(cls.users_group_name) ==
1445 1450 func.lower(group_name))
1446 1451
1447 1452 else:
1448 1453 q = cls.query().filter(cls.users_group_name == group_name)
1449 1454 if cache:
1450 1455 q = q.options(
1451 1456 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1452 1457 return q.scalar()
1453 1458
1454 1459 @classmethod
1455 1460 def get(cls, user_group_id, cache=False):
1456 1461 if not user_group_id:
1457 1462 return
1458 1463
1459 1464 user_group = cls.query()
1460 1465 if cache:
1461 1466 user_group = user_group.options(
1462 1467 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1463 1468 return user_group.get(user_group_id)
1464 1469
1465 1470 def permissions(self, with_admins=True, with_owner=True,
1466 1471 expand_from_user_groups=False):
1467 1472 """
1468 1473 Permissions for user groups
1469 1474 """
1470 1475 _admin_perm = 'usergroup.admin'
1471 1476
1472 1477 owner_row = []
1473 1478 if with_owner:
1474 1479 usr = AttributeDict(self.user.get_dict())
1475 1480 usr.owner_row = True
1476 1481 usr.permission = _admin_perm
1477 1482 owner_row.append(usr)
1478 1483
1479 1484 super_admin_ids = []
1480 1485 super_admin_rows = []
1481 1486 if with_admins:
1482 1487 for usr in User.get_all_super_admins():
1483 1488 super_admin_ids.append(usr.user_id)
1484 1489 # if this admin is also owner, don't double the record
1485 1490 if usr.user_id == owner_row[0].user_id:
1486 1491 owner_row[0].admin_row = True
1487 1492 else:
1488 1493 usr = AttributeDict(usr.get_dict())
1489 1494 usr.admin_row = True
1490 1495 usr.permission = _admin_perm
1491 1496 super_admin_rows.append(usr)
1492 1497
1493 1498 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1494 1499 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1495 1500 joinedload(UserUserGroupToPerm.user),
1496 1501 joinedload(UserUserGroupToPerm.permission),)
1497 1502
1498 1503 # get owners and admins and permissions. We do a trick of re-writing
1499 1504 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1500 1505 # has a global reference and changing one object propagates to all
1501 1506 # others. This means if admin is also an owner admin_row that change
1502 1507 # would propagate to both objects
1503 1508 perm_rows = []
1504 1509 for _usr in q.all():
1505 1510 usr = AttributeDict(_usr.user.get_dict())
1506 1511 # if this user is also owner/admin, mark as duplicate record
1507 1512 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1508 1513 usr.duplicate_perm = True
1509 1514 usr.permission = _usr.permission.permission_name
1510 1515 perm_rows.append(usr)
1511 1516
1512 1517 # filter the perm rows by 'default' first and then sort them by
1513 1518 # admin,write,read,none permissions sorted again alphabetically in
1514 1519 # each group
1515 1520 perm_rows = sorted(perm_rows, key=display_user_sort)
1516 1521
1517 1522 user_groups_rows = []
1518 1523 if expand_from_user_groups:
1519 1524 for ug in self.permission_user_groups(with_members=True):
1520 1525 for user_data in ug.members:
1521 1526 user_groups_rows.append(user_data)
1522 1527
1523 1528 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1524 1529
1525 1530 def permission_user_groups(self, with_members=False):
1526 1531 q = UserGroupUserGroupToPerm.query()\
1527 1532 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1528 1533 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1529 1534 joinedload(UserGroupUserGroupToPerm.target_user_group),
1530 1535 joinedload(UserGroupUserGroupToPerm.permission),)
1531 1536
1532 1537 perm_rows = []
1533 1538 for _user_group in q.all():
1534 1539 entry = AttributeDict(_user_group.user_group.get_dict())
1535 1540 entry.permission = _user_group.permission.permission_name
1536 1541 if with_members:
1537 1542 entry.members = [x.user.get_dict()
1538 1543 for x in _user_group.user_group.members]
1539 1544 perm_rows.append(entry)
1540 1545
1541 1546 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1542 1547 return perm_rows
1543 1548
1544 1549 def _get_default_perms(self, user_group, suffix=''):
1545 1550 from rhodecode.model.permission import PermissionModel
1546 1551 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1547 1552
1548 1553 def get_default_perms(self, suffix=''):
1549 1554 return self._get_default_perms(self, suffix)
1550 1555
1551 1556 def get_api_data(self, with_group_members=True, include_secrets=False):
1552 1557 """
1553 1558 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1554 1559 basically forwarded.
1555 1560
1556 1561 """
1557 1562 user_group = self
1558 1563 data = {
1559 1564 'users_group_id': user_group.users_group_id,
1560 1565 'group_name': user_group.users_group_name,
1561 1566 'group_description': user_group.user_group_description,
1562 1567 'active': user_group.users_group_active,
1563 1568 'owner': user_group.user.username,
1564 1569 'sync': user_group.sync,
1565 1570 'owner_email': user_group.user.email,
1566 1571 }
1567 1572
1568 1573 if with_group_members:
1569 1574 users = []
1570 1575 for user in user_group.members:
1571 1576 user = user.user
1572 1577 users.append(user.get_api_data(include_secrets=include_secrets))
1573 1578 data['users'] = users
1574 1579
1575 1580 return data
1576 1581
1577 1582
1578 1583 class UserGroupMember(Base, BaseModel):
1579 1584 __tablename__ = 'users_groups_members'
1580 1585 __table_args__ = (
1581 1586 base_table_args,
1582 1587 )
1583 1588
1584 1589 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1585 1590 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1586 1591 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1587 1592
1588 1593 user = relationship('User', lazy='joined')
1589 1594 users_group = relationship('UserGroup')
1590 1595
1591 1596 def __init__(self, gr_id='', u_id=''):
1592 1597 self.users_group_id = gr_id
1593 1598 self.user_id = u_id
1594 1599
1595 1600
1596 1601 class RepositoryField(Base, BaseModel):
1597 1602 __tablename__ = 'repositories_fields'
1598 1603 __table_args__ = (
1599 1604 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1600 1605 base_table_args,
1601 1606 )
1602 1607
1603 1608 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1604 1609
1605 1610 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1606 1611 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1607 1612 field_key = Column("field_key", String(250))
1608 1613 field_label = Column("field_label", String(1024), nullable=False)
1609 1614 field_value = Column("field_value", String(10000), nullable=False)
1610 1615 field_desc = Column("field_desc", String(1024), nullable=False)
1611 1616 field_type = Column("field_type", String(255), nullable=False, unique=None)
1612 1617 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1613 1618
1614 1619 repository = relationship('Repository')
1615 1620
1616 1621 @property
1617 1622 def field_key_prefixed(self):
1618 1623 return 'ex_%s' % self.field_key
1619 1624
1620 1625 @classmethod
1621 1626 def un_prefix_key(cls, key):
1622 1627 if key.startswith(cls.PREFIX):
1623 1628 return key[len(cls.PREFIX):]
1624 1629 return key
1625 1630
1626 1631 @classmethod
1627 1632 def get_by_key_name(cls, key, repo):
1628 1633 row = cls.query()\
1629 1634 .filter(cls.repository == repo)\
1630 1635 .filter(cls.field_key == key).scalar()
1631 1636 return row
1632 1637
1633 1638
1634 1639 class Repository(Base, BaseModel):
1635 1640 __tablename__ = 'repositories'
1636 1641 __table_args__ = (
1637 1642 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1638 1643 base_table_args,
1639 1644 )
1640 1645 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1641 1646 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1642 1647 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1643 1648
1644 1649 STATE_CREATED = 'repo_state_created'
1645 1650 STATE_PENDING = 'repo_state_pending'
1646 1651 STATE_ERROR = 'repo_state_error'
1647 1652
1648 1653 LOCK_AUTOMATIC = 'lock_auto'
1649 1654 LOCK_API = 'lock_api'
1650 1655 LOCK_WEB = 'lock_web'
1651 1656 LOCK_PULL = 'lock_pull'
1652 1657
1653 1658 NAME_SEP = URL_SEP
1654 1659
1655 1660 repo_id = Column(
1656 1661 "repo_id", Integer(), nullable=False, unique=True, default=None,
1657 1662 primary_key=True)
1658 1663 _repo_name = Column(
1659 1664 "repo_name", Text(), nullable=False, default=None)
1660 1665 repo_name_hash = Column(
1661 1666 "repo_name_hash", String(255), nullable=False, unique=True)
1662 1667 repo_state = Column("repo_state", String(255), nullable=True)
1663 1668
1664 1669 clone_uri = Column(
1665 1670 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1666 1671 default=None)
1667 1672 push_uri = Column(
1668 1673 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1669 1674 default=None)
1670 1675 repo_type = Column(
1671 1676 "repo_type", String(255), nullable=False, unique=False, default=None)
1672 1677 user_id = Column(
1673 1678 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1674 1679 unique=False, default=None)
1675 1680 private = Column(
1676 1681 "private", Boolean(), nullable=True, unique=None, default=None)
1677 1682 archived = Column(
1678 1683 "archived", Boolean(), nullable=True, unique=None, default=None)
1679 1684 enable_statistics = Column(
1680 1685 "statistics", Boolean(), nullable=True, unique=None, default=True)
1681 1686 enable_downloads = Column(
1682 1687 "downloads", Boolean(), nullable=True, unique=None, default=True)
1683 1688 description = Column(
1684 1689 "description", String(10000), nullable=True, unique=None, default=None)
1685 1690 created_on = Column(
1686 1691 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1687 1692 default=datetime.datetime.now)
1688 1693 updated_on = Column(
1689 1694 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1690 1695 default=datetime.datetime.now)
1691 1696 _landing_revision = Column(
1692 1697 "landing_revision", String(255), nullable=False, unique=False,
1693 1698 default=None)
1694 1699 enable_locking = Column(
1695 1700 "enable_locking", Boolean(), nullable=False, unique=None,
1696 1701 default=False)
1697 1702 _locked = Column(
1698 1703 "locked", String(255), nullable=True, unique=False, default=None)
1699 1704 _changeset_cache = Column(
1700 1705 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1701 1706
1702 1707 fork_id = Column(
1703 1708 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1704 1709 nullable=True, unique=False, default=None)
1705 1710 group_id = Column(
1706 1711 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1707 1712 unique=False, default=None)
1708 1713
1709 1714 user = relationship('User', lazy='joined')
1710 1715 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1711 1716 group = relationship('RepoGroup', lazy='joined')
1712 1717 repo_to_perm = relationship(
1713 1718 'UserRepoToPerm', cascade='all',
1714 1719 order_by='UserRepoToPerm.repo_to_perm_id')
1715 1720 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1716 1721 stats = relationship('Statistics', cascade='all', uselist=False)
1717 1722
1718 1723 followers = relationship(
1719 1724 'UserFollowing',
1720 1725 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1721 1726 cascade='all')
1722 1727 extra_fields = relationship(
1723 1728 'RepositoryField', cascade="all, delete-orphan")
1724 1729 logs = relationship('UserLog')
1725 1730 comments = relationship(
1726 1731 'ChangesetComment', cascade="all, delete-orphan")
1727 1732 pull_requests_source = relationship(
1728 1733 'PullRequest',
1729 1734 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1730 1735 cascade="all, delete-orphan")
1731 1736 pull_requests_target = relationship(
1732 1737 'PullRequest',
1733 1738 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1734 1739 cascade="all, delete-orphan")
1735 1740 ui = relationship('RepoRhodeCodeUi', cascade="all")
1736 1741 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1737 1742 integrations = relationship('Integration', cascade="all, delete-orphan")
1738 1743
1739 1744 scoped_tokens = relationship('UserApiKeys', cascade="all")
1740 1745
1741 1746 # no cascade, set NULL
1742 1747 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1743 1748
1744 1749 def __unicode__(self):
1745 1750 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1746 1751 safe_unicode(self.repo_name))
1747 1752
1748 1753 @hybrid_property
1749 1754 def description_safe(self):
1750 1755 from rhodecode.lib import helpers as h
1751 1756 return h.escape(self.description)
1752 1757
1753 1758 @hybrid_property
1754 1759 def landing_rev(self):
1755 1760 # always should return [rev_type, rev]
1756 1761 if self._landing_revision:
1757 1762 _rev_info = self._landing_revision.split(':')
1758 1763 if len(_rev_info) < 2:
1759 1764 _rev_info.insert(0, 'rev')
1760 1765 return [_rev_info[0], _rev_info[1]]
1761 1766 return [None, None]
1762 1767
1763 1768 @landing_rev.setter
1764 1769 def landing_rev(self, val):
1765 1770 if ':' not in val:
1766 1771 raise ValueError('value must be delimited with `:` and consist '
1767 1772 'of <rev_type>:<rev>, got %s instead' % val)
1768 1773 self._landing_revision = val
1769 1774
1770 1775 @hybrid_property
1771 1776 def locked(self):
1772 1777 if self._locked:
1773 1778 user_id, timelocked, reason = self._locked.split(':')
1774 1779 lock_values = int(user_id), timelocked, reason
1775 1780 else:
1776 1781 lock_values = [None, None, None]
1777 1782 return lock_values
1778 1783
1779 1784 @locked.setter
1780 1785 def locked(self, val):
1781 1786 if val and isinstance(val, (list, tuple)):
1782 1787 self._locked = ':'.join(map(str, val))
1783 1788 else:
1784 1789 self._locked = None
1785 1790
1786 1791 @classmethod
1787 1792 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1788 1793 from rhodecode.lib.vcs.backends.base import EmptyCommit
1789 1794 dummy = EmptyCommit().__json__()
1790 1795 if not changeset_cache_raw:
1791 1796 dummy['source_repo_id'] = repo_id
1792 1797 return json.loads(json.dumps(dummy))
1793 1798
1794 1799 try:
1795 1800 return json.loads(changeset_cache_raw)
1796 1801 except TypeError:
1797 1802 return dummy
1798 1803 except Exception:
1799 1804 log.error(traceback.format_exc())
1800 1805 return dummy
1801 1806
1802 1807 @hybrid_property
1803 1808 def changeset_cache(self):
1804 1809 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1805 1810
1806 1811 @changeset_cache.setter
1807 1812 def changeset_cache(self, val):
1808 1813 try:
1809 1814 self._changeset_cache = json.dumps(val)
1810 1815 except Exception:
1811 1816 log.error(traceback.format_exc())
1812 1817
1813 1818 @hybrid_property
1814 1819 def repo_name(self):
1815 1820 return self._repo_name
1816 1821
1817 1822 @repo_name.setter
1818 1823 def repo_name(self, value):
1819 1824 self._repo_name = value
1820 1825 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1821 1826
1822 1827 @classmethod
1823 1828 def normalize_repo_name(cls, repo_name):
1824 1829 """
1825 1830 Normalizes os specific repo_name to the format internally stored inside
1826 1831 database using URL_SEP
1827 1832
1828 1833 :param cls:
1829 1834 :param repo_name:
1830 1835 """
1831 1836 return cls.NAME_SEP.join(repo_name.split(os.sep))
1832 1837
1833 1838 @classmethod
1834 1839 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1835 1840 session = Session()
1836 1841 q = session.query(cls).filter(cls.repo_name == repo_name)
1837 1842
1838 1843 if cache:
1839 1844 if identity_cache:
1840 1845 val = cls.identity_cache(session, 'repo_name', repo_name)
1841 1846 if val:
1842 1847 return val
1843 1848 else:
1844 1849 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1845 1850 q = q.options(
1846 1851 FromCache("sql_cache_short", cache_key))
1847 1852
1848 1853 return q.scalar()
1849 1854
1850 1855 @classmethod
1851 1856 def get_by_id_or_repo_name(cls, repoid):
1852 1857 if isinstance(repoid, (int, long)):
1853 1858 try:
1854 1859 repo = cls.get(repoid)
1855 1860 except ValueError:
1856 1861 repo = None
1857 1862 else:
1858 1863 repo = cls.get_by_repo_name(repoid)
1859 1864 return repo
1860 1865
1861 1866 @classmethod
1862 1867 def get_by_full_path(cls, repo_full_path):
1863 1868 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1864 1869 repo_name = cls.normalize_repo_name(repo_name)
1865 1870 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1866 1871
1867 1872 @classmethod
1868 1873 def get_repo_forks(cls, repo_id):
1869 1874 return cls.query().filter(Repository.fork_id == repo_id)
1870 1875
1871 1876 @classmethod
1872 1877 def base_path(cls):
1873 1878 """
1874 1879 Returns base path when all repos are stored
1875 1880
1876 1881 :param cls:
1877 1882 """
1878 1883 q = Session().query(RhodeCodeUi)\
1879 1884 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1880 1885 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1881 1886 return q.one().ui_value
1882 1887
1883 1888 @classmethod
1884 1889 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1885 1890 case_insensitive=True, archived=False):
1886 1891 q = Repository.query()
1887 1892
1888 1893 if not archived:
1889 1894 q = q.filter(Repository.archived.isnot(true()))
1890 1895
1891 1896 if not isinstance(user_id, Optional):
1892 1897 q = q.filter(Repository.user_id == user_id)
1893 1898
1894 1899 if not isinstance(group_id, Optional):
1895 1900 q = q.filter(Repository.group_id == group_id)
1896 1901
1897 1902 if case_insensitive:
1898 1903 q = q.order_by(func.lower(Repository.repo_name))
1899 1904 else:
1900 1905 q = q.order_by(Repository.repo_name)
1901 1906
1902 1907 return q.all()
1903 1908
1904 1909 @property
1905 1910 def repo_uid(self):
1906 1911 return '_{}'.format(self.repo_id)
1907 1912
1908 1913 @property
1909 1914 def forks(self):
1910 1915 """
1911 1916 Return forks of this repo
1912 1917 """
1913 1918 return Repository.get_repo_forks(self.repo_id)
1914 1919
1915 1920 @property
1916 1921 def parent(self):
1917 1922 """
1918 1923 Returns fork parent
1919 1924 """
1920 1925 return self.fork
1921 1926
1922 1927 @property
1923 1928 def just_name(self):
1924 1929 return self.repo_name.split(self.NAME_SEP)[-1]
1925 1930
1926 1931 @property
1927 1932 def groups_with_parents(self):
1928 1933 groups = []
1929 1934 if self.group is None:
1930 1935 return groups
1931 1936
1932 1937 cur_gr = self.group
1933 1938 groups.insert(0, cur_gr)
1934 1939 while 1:
1935 1940 gr = getattr(cur_gr, 'parent_group', None)
1936 1941 cur_gr = cur_gr.parent_group
1937 1942 if gr is None:
1938 1943 break
1939 1944 groups.insert(0, gr)
1940 1945
1941 1946 return groups
1942 1947
1943 1948 @property
1944 1949 def groups_and_repo(self):
1945 1950 return self.groups_with_parents, self
1946 1951
1947 1952 @LazyProperty
1948 1953 def repo_path(self):
1949 1954 """
1950 1955 Returns base full path for that repository means where it actually
1951 1956 exists on a filesystem
1952 1957 """
1953 1958 q = Session().query(RhodeCodeUi).filter(
1954 1959 RhodeCodeUi.ui_key == self.NAME_SEP)
1955 1960 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1956 1961 return q.one().ui_value
1957 1962
1958 1963 @property
1959 1964 def repo_full_path(self):
1960 1965 p = [self.repo_path]
1961 1966 # we need to split the name by / since this is how we store the
1962 1967 # names in the database, but that eventually needs to be converted
1963 1968 # into a valid system path
1964 1969 p += self.repo_name.split(self.NAME_SEP)
1965 1970 return os.path.join(*map(safe_unicode, p))
1966 1971
1967 1972 @property
1968 1973 def cache_keys(self):
1969 1974 """
1970 1975 Returns associated cache keys for that repo
1971 1976 """
1972 1977 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1973 1978 repo_id=self.repo_id)
1974 1979 return CacheKey.query()\
1975 1980 .filter(CacheKey.cache_args == invalidation_namespace)\
1976 1981 .order_by(CacheKey.cache_key)\
1977 1982 .all()
1978 1983
1979 1984 @property
1980 1985 def cached_diffs_relative_dir(self):
1981 1986 """
1982 1987 Return a relative to the repository store path of cached diffs
1983 1988 used for safe display for users, who shouldn't know the absolute store
1984 1989 path
1985 1990 """
1986 1991 return os.path.join(
1987 1992 os.path.dirname(self.repo_name),
1988 1993 self.cached_diffs_dir.split(os.path.sep)[-1])
1989 1994
1990 1995 @property
1991 1996 def cached_diffs_dir(self):
1992 1997 path = self.repo_full_path
1993 1998 return os.path.join(
1994 1999 os.path.dirname(path),
1995 2000 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1996 2001
1997 2002 def cached_diffs(self):
1998 2003 diff_cache_dir = self.cached_diffs_dir
1999 2004 if os.path.isdir(diff_cache_dir):
2000 2005 return os.listdir(diff_cache_dir)
2001 2006 return []
2002 2007
2003 2008 def shadow_repos(self):
2004 2009 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2005 2010 return [
2006 2011 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2007 2012 if x.startswith(shadow_repos_pattern)]
2008 2013
2009 2014 def get_new_name(self, repo_name):
2010 2015 """
2011 2016 returns new full repository name based on assigned group and new new
2012 2017
2013 2018 :param group_name:
2014 2019 """
2015 2020 path_prefix = self.group.full_path_splitted if self.group else []
2016 2021 return self.NAME_SEP.join(path_prefix + [repo_name])
2017 2022
2018 2023 @property
2019 2024 def _config(self):
2020 2025 """
2021 2026 Returns db based config object.
2022 2027 """
2023 2028 from rhodecode.lib.utils import make_db_config
2024 2029 return make_db_config(clear_session=False, repo=self)
2025 2030
2026 2031 def permissions(self, with_admins=True, with_owner=True,
2027 2032 expand_from_user_groups=False):
2028 2033 """
2029 2034 Permissions for repositories
2030 2035 """
2031 2036 _admin_perm = 'repository.admin'
2032 2037
2033 2038 owner_row = []
2034 2039 if with_owner:
2035 2040 usr = AttributeDict(self.user.get_dict())
2036 2041 usr.owner_row = True
2037 2042 usr.permission = _admin_perm
2038 2043 usr.permission_id = None
2039 2044 owner_row.append(usr)
2040 2045
2041 2046 super_admin_ids = []
2042 2047 super_admin_rows = []
2043 2048 if with_admins:
2044 2049 for usr in User.get_all_super_admins():
2045 2050 super_admin_ids.append(usr.user_id)
2046 2051 # if this admin is also owner, don't double the record
2047 2052 if usr.user_id == owner_row[0].user_id:
2048 2053 owner_row[0].admin_row = True
2049 2054 else:
2050 2055 usr = AttributeDict(usr.get_dict())
2051 2056 usr.admin_row = True
2052 2057 usr.permission = _admin_perm
2053 2058 usr.permission_id = None
2054 2059 super_admin_rows.append(usr)
2055 2060
2056 2061 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2057 2062 q = q.options(joinedload(UserRepoToPerm.repository),
2058 2063 joinedload(UserRepoToPerm.user),
2059 2064 joinedload(UserRepoToPerm.permission),)
2060 2065
2061 2066 # get owners and admins and permissions. We do a trick of re-writing
2062 2067 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2063 2068 # has a global reference and changing one object propagates to all
2064 2069 # others. This means if admin is also an owner admin_row that change
2065 2070 # would propagate to both objects
2066 2071 perm_rows = []
2067 2072 for _usr in q.all():
2068 2073 usr = AttributeDict(_usr.user.get_dict())
2069 2074 # if this user is also owner/admin, mark as duplicate record
2070 2075 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2071 2076 usr.duplicate_perm = True
2072 2077 # also check if this permission is maybe used by branch_permissions
2073 2078 if _usr.branch_perm_entry:
2074 2079 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2075 2080
2076 2081 usr.permission = _usr.permission.permission_name
2077 2082 usr.permission_id = _usr.repo_to_perm_id
2078 2083 perm_rows.append(usr)
2079 2084
2080 2085 # filter the perm rows by 'default' first and then sort them by
2081 2086 # admin,write,read,none permissions sorted again alphabetically in
2082 2087 # each group
2083 2088 perm_rows = sorted(perm_rows, key=display_user_sort)
2084 2089
2085 2090 user_groups_rows = []
2086 2091 if expand_from_user_groups:
2087 2092 for ug in self.permission_user_groups(with_members=True):
2088 2093 for user_data in ug.members:
2089 2094 user_groups_rows.append(user_data)
2090 2095
2091 2096 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2092 2097
2093 2098 def permission_user_groups(self, with_members=True):
2094 2099 q = UserGroupRepoToPerm.query()\
2095 2100 .filter(UserGroupRepoToPerm.repository == self)
2096 2101 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2097 2102 joinedload(UserGroupRepoToPerm.users_group),
2098 2103 joinedload(UserGroupRepoToPerm.permission),)
2099 2104
2100 2105 perm_rows = []
2101 2106 for _user_group in q.all():
2102 2107 entry = AttributeDict(_user_group.users_group.get_dict())
2103 2108 entry.permission = _user_group.permission.permission_name
2104 2109 if with_members:
2105 2110 entry.members = [x.user.get_dict()
2106 2111 for x in _user_group.users_group.members]
2107 2112 perm_rows.append(entry)
2108 2113
2109 2114 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2110 2115 return perm_rows
2111 2116
2112 2117 def get_api_data(self, include_secrets=False):
2113 2118 """
2114 2119 Common function for generating repo api data
2115 2120
2116 2121 :param include_secrets: See :meth:`User.get_api_data`.
2117 2122
2118 2123 """
2119 2124 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2120 2125 # move this methods on models level.
2121 2126 from rhodecode.model.settings import SettingsModel
2122 2127 from rhodecode.model.repo import RepoModel
2123 2128
2124 2129 repo = self
2125 2130 _user_id, _time, _reason = self.locked
2126 2131
2127 2132 data = {
2128 2133 'repo_id': repo.repo_id,
2129 2134 'repo_name': repo.repo_name,
2130 2135 'repo_type': repo.repo_type,
2131 2136 'clone_uri': repo.clone_uri or '',
2132 2137 'push_uri': repo.push_uri or '',
2133 2138 'url': RepoModel().get_url(self),
2134 2139 'private': repo.private,
2135 2140 'created_on': repo.created_on,
2136 2141 'description': repo.description_safe,
2137 2142 'landing_rev': repo.landing_rev,
2138 2143 'owner': repo.user.username,
2139 2144 'fork_of': repo.fork.repo_name if repo.fork else None,
2140 2145 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2141 2146 'enable_statistics': repo.enable_statistics,
2142 2147 'enable_locking': repo.enable_locking,
2143 2148 'enable_downloads': repo.enable_downloads,
2144 2149 'last_changeset': repo.changeset_cache,
2145 2150 'locked_by': User.get(_user_id).get_api_data(
2146 2151 include_secrets=include_secrets) if _user_id else None,
2147 2152 'locked_date': time_to_datetime(_time) if _time else None,
2148 2153 'lock_reason': _reason if _reason else None,
2149 2154 }
2150 2155
2151 2156 # TODO: mikhail: should be per-repo settings here
2152 2157 rc_config = SettingsModel().get_all_settings()
2153 2158 repository_fields = str2bool(
2154 2159 rc_config.get('rhodecode_repository_fields'))
2155 2160 if repository_fields:
2156 2161 for f in self.extra_fields:
2157 2162 data[f.field_key_prefixed] = f.field_value
2158 2163
2159 2164 return data
2160 2165
2161 2166 @classmethod
2162 2167 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2163 2168 if not lock_time:
2164 2169 lock_time = time.time()
2165 2170 if not lock_reason:
2166 2171 lock_reason = cls.LOCK_AUTOMATIC
2167 2172 repo.locked = [user_id, lock_time, lock_reason]
2168 2173 Session().add(repo)
2169 2174 Session().commit()
2170 2175
2171 2176 @classmethod
2172 2177 def unlock(cls, repo):
2173 2178 repo.locked = None
2174 2179 Session().add(repo)
2175 2180 Session().commit()
2176 2181
2177 2182 @classmethod
2178 2183 def getlock(cls, repo):
2179 2184 return repo.locked
2180 2185
2181 2186 def is_user_lock(self, user_id):
2182 2187 if self.lock[0]:
2183 2188 lock_user_id = safe_int(self.lock[0])
2184 2189 user_id = safe_int(user_id)
2185 2190 # both are ints, and they are equal
2186 2191 return all([lock_user_id, user_id]) and lock_user_id == user_id
2187 2192
2188 2193 return False
2189 2194
2190 2195 def get_locking_state(self, action, user_id, only_when_enabled=True):
2191 2196 """
2192 2197 Checks locking on this repository, if locking is enabled and lock is
2193 2198 present returns a tuple of make_lock, locked, locked_by.
2194 2199 make_lock can have 3 states None (do nothing) True, make lock
2195 2200 False release lock, This value is later propagated to hooks, which
2196 2201 do the locking. Think about this as signals passed to hooks what to do.
2197 2202
2198 2203 """
2199 2204 # TODO: johbo: This is part of the business logic and should be moved
2200 2205 # into the RepositoryModel.
2201 2206
2202 2207 if action not in ('push', 'pull'):
2203 2208 raise ValueError("Invalid action value: %s" % repr(action))
2204 2209
2205 2210 # defines if locked error should be thrown to user
2206 2211 currently_locked = False
2207 2212 # defines if new lock should be made, tri-state
2208 2213 make_lock = None
2209 2214 repo = self
2210 2215 user = User.get(user_id)
2211 2216
2212 2217 lock_info = repo.locked
2213 2218
2214 2219 if repo and (repo.enable_locking or not only_when_enabled):
2215 2220 if action == 'push':
2216 2221 # check if it's already locked !, if it is compare users
2217 2222 locked_by_user_id = lock_info[0]
2218 2223 if user.user_id == locked_by_user_id:
2219 2224 log.debug(
2220 2225 'Got `push` action from user %s, now unlocking', user)
2221 2226 # unlock if we have push from user who locked
2222 2227 make_lock = False
2223 2228 else:
2224 2229 # we're not the same user who locked, ban with
2225 2230 # code defined in settings (default is 423 HTTP Locked) !
2226 2231 log.debug('Repo %s is currently locked by %s', repo, user)
2227 2232 currently_locked = True
2228 2233 elif action == 'pull':
2229 2234 # [0] user [1] date
2230 2235 if lock_info[0] and lock_info[1]:
2231 2236 log.debug('Repo %s is currently locked by %s', repo, user)
2232 2237 currently_locked = True
2233 2238 else:
2234 2239 log.debug('Setting lock on repo %s by %s', repo, user)
2235 2240 make_lock = True
2236 2241
2237 2242 else:
2238 2243 log.debug('Repository %s do not have locking enabled', repo)
2239 2244
2240 2245 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2241 2246 make_lock, currently_locked, lock_info)
2242 2247
2243 2248 from rhodecode.lib.auth import HasRepoPermissionAny
2244 2249 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2245 2250 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2246 2251 # if we don't have at least write permission we cannot make a lock
2247 2252 log.debug('lock state reset back to FALSE due to lack '
2248 2253 'of at least read permission')
2249 2254 make_lock = False
2250 2255
2251 2256 return make_lock, currently_locked, lock_info
2252 2257
2253 2258 @property
2254 2259 def last_commit_cache_update_diff(self):
2255 2260 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2256 2261
2257 2262 @classmethod
2258 2263 def _load_commit_change(cls, last_commit_cache):
2259 2264 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2260 2265 empty_date = datetime.datetime.fromtimestamp(0)
2261 2266 date_latest = last_commit_cache.get('date', empty_date)
2262 2267 try:
2263 2268 return parse_datetime(date_latest)
2264 2269 except Exception:
2265 2270 return empty_date
2266 2271
2267 2272 @property
2268 2273 def last_commit_change(self):
2269 2274 return self._load_commit_change(self.changeset_cache)
2270 2275
2271 2276 @property
2272 2277 def last_db_change(self):
2273 2278 return self.updated_on
2274 2279
2275 2280 @property
2276 2281 def clone_uri_hidden(self):
2277 2282 clone_uri = self.clone_uri
2278 2283 if clone_uri:
2279 2284 import urlobject
2280 2285 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2281 2286 if url_obj.password:
2282 2287 clone_uri = url_obj.with_password('*****')
2283 2288 return clone_uri
2284 2289
2285 2290 @property
2286 2291 def push_uri_hidden(self):
2287 2292 push_uri = self.push_uri
2288 2293 if push_uri:
2289 2294 import urlobject
2290 2295 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2291 2296 if url_obj.password:
2292 2297 push_uri = url_obj.with_password('*****')
2293 2298 return push_uri
2294 2299
2295 2300 def clone_url(self, **override):
2296 2301 from rhodecode.model.settings import SettingsModel
2297 2302
2298 2303 uri_tmpl = None
2299 2304 if 'with_id' in override:
2300 2305 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2301 2306 del override['with_id']
2302 2307
2303 2308 if 'uri_tmpl' in override:
2304 2309 uri_tmpl = override['uri_tmpl']
2305 2310 del override['uri_tmpl']
2306 2311
2307 2312 ssh = False
2308 2313 if 'ssh' in override:
2309 2314 ssh = True
2310 2315 del override['ssh']
2311 2316
2312 2317 # we didn't override our tmpl from **overrides
2313 2318 request = get_current_request()
2314 2319 if not uri_tmpl:
2315 2320 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2316 2321 rc_config = request.call_context.rc_config
2317 2322 else:
2318 2323 rc_config = SettingsModel().get_all_settings(cache=True)
2319 2324
2320 2325 if ssh:
2321 2326 uri_tmpl = rc_config.get(
2322 2327 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2323 2328
2324 2329 else:
2325 2330 uri_tmpl = rc_config.get(
2326 2331 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2327 2332
2328 2333 return get_clone_url(request=request,
2329 2334 uri_tmpl=uri_tmpl,
2330 2335 repo_name=self.repo_name,
2331 2336 repo_id=self.repo_id,
2332 2337 repo_type=self.repo_type,
2333 2338 **override)
2334 2339
2335 2340 def set_state(self, state):
2336 2341 self.repo_state = state
2337 2342 Session().add(self)
2338 2343 #==========================================================================
2339 2344 # SCM PROPERTIES
2340 2345 #==========================================================================
2341 2346
2342 2347 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2343 2348 return get_commit_safe(
2344 2349 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2345 2350 maybe_unreachable=maybe_unreachable)
2346 2351
2347 2352 def get_changeset(self, rev=None, pre_load=None):
2348 2353 warnings.warn("Use get_commit", DeprecationWarning)
2349 2354 commit_id = None
2350 2355 commit_idx = None
2351 2356 if isinstance(rev, compat.string_types):
2352 2357 commit_id = rev
2353 2358 else:
2354 2359 commit_idx = rev
2355 2360 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2356 2361 pre_load=pre_load)
2357 2362
2358 2363 def get_landing_commit(self):
2359 2364 """
2360 2365 Returns landing commit, or if that doesn't exist returns the tip
2361 2366 """
2362 2367 _rev_type, _rev = self.landing_rev
2363 2368 commit = self.get_commit(_rev)
2364 2369 if isinstance(commit, EmptyCommit):
2365 2370 return self.get_commit()
2366 2371 return commit
2367 2372
2368 2373 def flush_commit_cache(self):
2369 2374 self.update_commit_cache(cs_cache={'raw_id':'0'})
2370 2375 self.update_commit_cache()
2371 2376
2372 2377 def update_commit_cache(self, cs_cache=None, config=None):
2373 2378 """
2374 2379 Update cache of last commit for repository
2375 2380 cache_keys should be::
2376 2381
2377 2382 source_repo_id
2378 2383 short_id
2379 2384 raw_id
2380 2385 revision
2381 2386 parents
2382 2387 message
2383 2388 date
2384 2389 author
2385 2390 updated_on
2386 2391
2387 2392 """
2388 2393 from rhodecode.lib.vcs.backends.base import BaseChangeset
2389 2394 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2390 2395 empty_date = datetime.datetime.fromtimestamp(0)
2391 2396
2392 2397 if cs_cache is None:
2393 2398 # use no-cache version here
2394 2399 try:
2395 2400 scm_repo = self.scm_instance(cache=False, config=config)
2396 2401 except VCSError:
2397 2402 scm_repo = None
2398 2403 empty = scm_repo is None or scm_repo.is_empty()
2399 2404
2400 2405 if not empty:
2401 2406 cs_cache = scm_repo.get_commit(
2402 2407 pre_load=["author", "date", "message", "parents", "branch"])
2403 2408 else:
2404 2409 cs_cache = EmptyCommit()
2405 2410
2406 2411 if isinstance(cs_cache, BaseChangeset):
2407 2412 cs_cache = cs_cache.__json__()
2408 2413
2409 2414 def is_outdated(new_cs_cache):
2410 2415 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2411 2416 new_cs_cache['revision'] != self.changeset_cache['revision']):
2412 2417 return True
2413 2418 return False
2414 2419
2415 2420 # check if we have maybe already latest cached revision
2416 2421 if is_outdated(cs_cache) or not self.changeset_cache:
2417 2422 _current_datetime = datetime.datetime.utcnow()
2418 2423 last_change = cs_cache.get('date') or _current_datetime
2419 2424 # we check if last update is newer than the new value
2420 2425 # if yes, we use the current timestamp instead. Imagine you get
2421 2426 # old commit pushed 1y ago, we'd set last update 1y to ago.
2422 2427 last_change_timestamp = datetime_to_time(last_change)
2423 2428 current_timestamp = datetime_to_time(last_change)
2424 2429 if last_change_timestamp > current_timestamp and not empty:
2425 2430 cs_cache['date'] = _current_datetime
2426 2431
2427 2432 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2428 2433 cs_cache['updated_on'] = time.time()
2429 2434 self.changeset_cache = cs_cache
2430 2435 self.updated_on = last_change
2431 2436 Session().add(self)
2432 2437 Session().commit()
2433 2438
2434 2439 else:
2435 2440 if empty:
2436 2441 cs_cache = EmptyCommit().__json__()
2437 2442 else:
2438 2443 cs_cache = self.changeset_cache
2439 2444
2440 2445 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2441 2446
2442 2447 cs_cache['updated_on'] = time.time()
2443 2448 self.changeset_cache = cs_cache
2444 2449 self.updated_on = _date_latest
2445 2450 Session().add(self)
2446 2451 Session().commit()
2447 2452
2448 2453 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2449 2454 self.repo_name, cs_cache, _date_latest)
2450 2455
2451 2456 @property
2452 2457 def tip(self):
2453 2458 return self.get_commit('tip')
2454 2459
2455 2460 @property
2456 2461 def author(self):
2457 2462 return self.tip.author
2458 2463
2459 2464 @property
2460 2465 def last_change(self):
2461 2466 return self.scm_instance().last_change
2462 2467
2463 2468 def get_comments(self, revisions=None):
2464 2469 """
2465 2470 Returns comments for this repository grouped by revisions
2466 2471
2467 2472 :param revisions: filter query by revisions only
2468 2473 """
2469 2474 cmts = ChangesetComment.query()\
2470 2475 .filter(ChangesetComment.repo == self)
2471 2476 if revisions:
2472 2477 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2473 2478 grouped = collections.defaultdict(list)
2474 2479 for cmt in cmts.all():
2475 2480 grouped[cmt.revision].append(cmt)
2476 2481 return grouped
2477 2482
2478 2483 def statuses(self, revisions=None):
2479 2484 """
2480 2485 Returns statuses for this repository
2481 2486
2482 2487 :param revisions: list of revisions to get statuses for
2483 2488 """
2484 2489 statuses = ChangesetStatus.query()\
2485 2490 .filter(ChangesetStatus.repo == self)\
2486 2491 .filter(ChangesetStatus.version == 0)
2487 2492
2488 2493 if revisions:
2489 2494 # Try doing the filtering in chunks to avoid hitting limits
2490 2495 size = 500
2491 2496 status_results = []
2492 2497 for chunk in xrange(0, len(revisions), size):
2493 2498 status_results += statuses.filter(
2494 2499 ChangesetStatus.revision.in_(
2495 2500 revisions[chunk: chunk+size])
2496 2501 ).all()
2497 2502 else:
2498 2503 status_results = statuses.all()
2499 2504
2500 2505 grouped = {}
2501 2506
2502 2507 # maybe we have open new pullrequest without a status?
2503 2508 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2504 2509 status_lbl = ChangesetStatus.get_status_lbl(stat)
2505 2510 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2506 2511 for rev in pr.revisions:
2507 2512 pr_id = pr.pull_request_id
2508 2513 pr_repo = pr.target_repo.repo_name
2509 2514 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2510 2515
2511 2516 for stat in status_results:
2512 2517 pr_id = pr_repo = None
2513 2518 if stat.pull_request:
2514 2519 pr_id = stat.pull_request.pull_request_id
2515 2520 pr_repo = stat.pull_request.target_repo.repo_name
2516 2521 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2517 2522 pr_id, pr_repo]
2518 2523 return grouped
2519 2524
2520 2525 # ==========================================================================
2521 2526 # SCM CACHE INSTANCE
2522 2527 # ==========================================================================
2523 2528
2524 2529 def scm_instance(self, **kwargs):
2525 2530 import rhodecode
2526 2531
2527 2532 # Passing a config will not hit the cache currently only used
2528 2533 # for repo2dbmapper
2529 2534 config = kwargs.pop('config', None)
2530 2535 cache = kwargs.pop('cache', None)
2531 2536 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2532 2537 if vcs_full_cache is not None:
2533 2538 # allows override global config
2534 2539 full_cache = vcs_full_cache
2535 2540 else:
2536 2541 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2537 2542 # if cache is NOT defined use default global, else we have a full
2538 2543 # control over cache behaviour
2539 2544 if cache is None and full_cache and not config:
2540 2545 log.debug('Initializing pure cached instance for %s', self.repo_path)
2541 2546 return self._get_instance_cached()
2542 2547
2543 2548 # cache here is sent to the "vcs server"
2544 2549 return self._get_instance(cache=bool(cache), config=config)
2545 2550
2546 2551 def _get_instance_cached(self):
2547 2552 from rhodecode.lib import rc_cache
2548 2553
2549 2554 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2550 2555 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2551 2556 repo_id=self.repo_id)
2552 2557 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2553 2558
2554 2559 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2555 2560 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2556 2561 return self._get_instance(repo_state_uid=_cache_state_uid)
2557 2562
2558 2563 # we must use thread scoped cache here,
2559 2564 # because each thread of gevent needs it's own not shared connection and cache
2560 2565 # we also alter `args` so the cache key is individual for every green thread.
2561 2566 inv_context_manager = rc_cache.InvalidationContext(
2562 2567 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2563 2568 thread_scoped=True)
2564 2569 with inv_context_manager as invalidation_context:
2565 2570 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2566 2571 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2567 2572
2568 2573 # re-compute and store cache if we get invalidate signal
2569 2574 if invalidation_context.should_invalidate():
2570 2575 instance = get_instance_cached.refresh(*args)
2571 2576 else:
2572 2577 instance = get_instance_cached(*args)
2573 2578
2574 2579 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2575 2580 return instance
2576 2581
2577 2582 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2578 2583 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2579 2584 self.repo_type, self.repo_path, cache)
2580 2585 config = config or self._config
2581 2586 custom_wire = {
2582 2587 'cache': cache, # controls the vcs.remote cache
2583 2588 'repo_state_uid': repo_state_uid
2584 2589 }
2585 2590 repo = get_vcs_instance(
2586 2591 repo_path=safe_str(self.repo_full_path),
2587 2592 config=config,
2588 2593 with_wire=custom_wire,
2589 2594 create=False,
2590 2595 _vcs_alias=self.repo_type)
2591 2596 if repo is not None:
2592 2597 repo.count() # cache rebuild
2593 2598 return repo
2594 2599
2595 2600 def get_shadow_repository_path(self, workspace_id):
2596 2601 from rhodecode.lib.vcs.backends.base import BaseRepository
2597 2602 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2598 2603 self.repo_full_path, self.repo_id, workspace_id)
2599 2604 return shadow_repo_path
2600 2605
2601 2606 def __json__(self):
2602 2607 return {'landing_rev': self.landing_rev}
2603 2608
2604 2609 def get_dict(self):
2605 2610
2606 2611 # Since we transformed `repo_name` to a hybrid property, we need to
2607 2612 # keep compatibility with the code which uses `repo_name` field.
2608 2613
2609 2614 result = super(Repository, self).get_dict()
2610 2615 result['repo_name'] = result.pop('_repo_name', None)
2611 2616 return result
2612 2617
2613 2618
2614 2619 class RepoGroup(Base, BaseModel):
2615 2620 __tablename__ = 'groups'
2616 2621 __table_args__ = (
2617 2622 UniqueConstraint('group_name', 'group_parent_id'),
2618 2623 base_table_args,
2619 2624 )
2620 2625 __mapper_args__ = {'order_by': 'group_name'}
2621 2626
2622 2627 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2623 2628
2624 2629 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2625 2630 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2626 2631 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2627 2632 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2628 2633 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2629 2634 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2630 2635 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2631 2636 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2632 2637 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2633 2638 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2634 2639 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2635 2640
2636 2641 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2637 2642 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2638 2643 parent_group = relationship('RepoGroup', remote_side=group_id)
2639 2644 user = relationship('User')
2640 2645 integrations = relationship('Integration', cascade="all, delete-orphan")
2641 2646
2642 2647 # no cascade, set NULL
2643 2648 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2644 2649
2645 2650 def __init__(self, group_name='', parent_group=None):
2646 2651 self.group_name = group_name
2647 2652 self.parent_group = parent_group
2648 2653
2649 2654 def __unicode__(self):
2650 2655 return u"<%s('id:%s:%s')>" % (
2651 2656 self.__class__.__name__, self.group_id, self.group_name)
2652 2657
2653 2658 @hybrid_property
2654 2659 def group_name(self):
2655 2660 return self._group_name
2656 2661
2657 2662 @group_name.setter
2658 2663 def group_name(self, value):
2659 2664 self._group_name = value
2660 2665 self.group_name_hash = self.hash_repo_group_name(value)
2661 2666
2662 2667 @classmethod
2663 2668 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2664 2669 from rhodecode.lib.vcs.backends.base import EmptyCommit
2665 2670 dummy = EmptyCommit().__json__()
2666 2671 if not changeset_cache_raw:
2667 2672 dummy['source_repo_id'] = repo_id
2668 2673 return json.loads(json.dumps(dummy))
2669 2674
2670 2675 try:
2671 2676 return json.loads(changeset_cache_raw)
2672 2677 except TypeError:
2673 2678 return dummy
2674 2679 except Exception:
2675 2680 log.error(traceback.format_exc())
2676 2681 return dummy
2677 2682
2678 2683 @hybrid_property
2679 2684 def changeset_cache(self):
2680 2685 return self._load_changeset_cache('', self._changeset_cache)
2681 2686
2682 2687 @changeset_cache.setter
2683 2688 def changeset_cache(self, val):
2684 2689 try:
2685 2690 self._changeset_cache = json.dumps(val)
2686 2691 except Exception:
2687 2692 log.error(traceback.format_exc())
2688 2693
2689 2694 @validates('group_parent_id')
2690 2695 def validate_group_parent_id(self, key, val):
2691 2696 """
2692 2697 Check cycle references for a parent group to self
2693 2698 """
2694 2699 if self.group_id and val:
2695 2700 assert val != self.group_id
2696 2701
2697 2702 return val
2698 2703
2699 2704 @hybrid_property
2700 2705 def description_safe(self):
2701 2706 from rhodecode.lib import helpers as h
2702 2707 return h.escape(self.group_description)
2703 2708
2704 2709 @classmethod
2705 2710 def hash_repo_group_name(cls, repo_group_name):
2706 2711 val = remove_formatting(repo_group_name)
2707 2712 val = safe_str(val).lower()
2708 2713 chars = []
2709 2714 for c in val:
2710 2715 if c not in string.ascii_letters:
2711 2716 c = str(ord(c))
2712 2717 chars.append(c)
2713 2718
2714 2719 return ''.join(chars)
2715 2720
2716 2721 @classmethod
2717 2722 def _generate_choice(cls, repo_group):
2718 2723 from webhelpers2.html import literal as _literal
2719 2724 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2720 2725 return repo_group.group_id, _name(repo_group.full_path_splitted)
2721 2726
2722 2727 @classmethod
2723 2728 def groups_choices(cls, groups=None, show_empty_group=True):
2724 2729 if not groups:
2725 2730 groups = cls.query().all()
2726 2731
2727 2732 repo_groups = []
2728 2733 if show_empty_group:
2729 2734 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2730 2735
2731 2736 repo_groups.extend([cls._generate_choice(x) for x in groups])
2732 2737
2733 2738 repo_groups = sorted(
2734 2739 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2735 2740 return repo_groups
2736 2741
2737 2742 @classmethod
2738 2743 def url_sep(cls):
2739 2744 return URL_SEP
2740 2745
2741 2746 @classmethod
2742 2747 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2743 2748 if case_insensitive:
2744 2749 gr = cls.query().filter(func.lower(cls.group_name)
2745 2750 == func.lower(group_name))
2746 2751 else:
2747 2752 gr = cls.query().filter(cls.group_name == group_name)
2748 2753 if cache:
2749 2754 name_key = _hash_key(group_name)
2750 2755 gr = gr.options(
2751 2756 FromCache("sql_cache_short", "get_group_%s" % name_key))
2752 2757 return gr.scalar()
2753 2758
2754 2759 @classmethod
2755 2760 def get_user_personal_repo_group(cls, user_id):
2756 2761 user = User.get(user_id)
2757 2762 if user.username == User.DEFAULT_USER:
2758 2763 return None
2759 2764
2760 2765 return cls.query()\
2761 2766 .filter(cls.personal == true()) \
2762 2767 .filter(cls.user == user) \
2763 2768 .order_by(cls.group_id.asc()) \
2764 2769 .first()
2765 2770
2766 2771 @classmethod
2767 2772 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2768 2773 case_insensitive=True):
2769 2774 q = RepoGroup.query()
2770 2775
2771 2776 if not isinstance(user_id, Optional):
2772 2777 q = q.filter(RepoGroup.user_id == user_id)
2773 2778
2774 2779 if not isinstance(group_id, Optional):
2775 2780 q = q.filter(RepoGroup.group_parent_id == group_id)
2776 2781
2777 2782 if case_insensitive:
2778 2783 q = q.order_by(func.lower(RepoGroup.group_name))
2779 2784 else:
2780 2785 q = q.order_by(RepoGroup.group_name)
2781 2786 return q.all()
2782 2787
2783 2788 @property
2784 2789 def parents(self, parents_recursion_limit=10):
2785 2790 groups = []
2786 2791 if self.parent_group is None:
2787 2792 return groups
2788 2793 cur_gr = self.parent_group
2789 2794 groups.insert(0, cur_gr)
2790 2795 cnt = 0
2791 2796 while 1:
2792 2797 cnt += 1
2793 2798 gr = getattr(cur_gr, 'parent_group', None)
2794 2799 cur_gr = cur_gr.parent_group
2795 2800 if gr is None:
2796 2801 break
2797 2802 if cnt == parents_recursion_limit:
2798 2803 # this will prevent accidental infinit loops
2799 2804 log.error('more than %s parents found for group %s, stopping '
2800 2805 'recursive parent fetching', parents_recursion_limit, self)
2801 2806 break
2802 2807
2803 2808 groups.insert(0, gr)
2804 2809 return groups
2805 2810
2806 2811 @property
2807 2812 def last_commit_cache_update_diff(self):
2808 2813 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2809 2814
2810 2815 @classmethod
2811 2816 def _load_commit_change(cls, last_commit_cache):
2812 2817 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2813 2818 empty_date = datetime.datetime.fromtimestamp(0)
2814 2819 date_latest = last_commit_cache.get('date', empty_date)
2815 2820 try:
2816 2821 return parse_datetime(date_latest)
2817 2822 except Exception:
2818 2823 return empty_date
2819 2824
2820 2825 @property
2821 2826 def last_commit_change(self):
2822 2827 return self._load_commit_change(self.changeset_cache)
2823 2828
2824 2829 @property
2825 2830 def last_db_change(self):
2826 2831 return self.updated_on
2827 2832
2828 2833 @property
2829 2834 def children(self):
2830 2835 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2831 2836
2832 2837 @property
2833 2838 def name(self):
2834 2839 return self.group_name.split(RepoGroup.url_sep())[-1]
2835 2840
2836 2841 @property
2837 2842 def full_path(self):
2838 2843 return self.group_name
2839 2844
2840 2845 @property
2841 2846 def full_path_splitted(self):
2842 2847 return self.group_name.split(RepoGroup.url_sep())
2843 2848
2844 2849 @property
2845 2850 def repositories(self):
2846 2851 return Repository.query()\
2847 2852 .filter(Repository.group == self)\
2848 2853 .order_by(Repository.repo_name)
2849 2854
2850 2855 @property
2851 2856 def repositories_recursive_count(self):
2852 2857 cnt = self.repositories.count()
2853 2858
2854 2859 def children_count(group):
2855 2860 cnt = 0
2856 2861 for child in group.children:
2857 2862 cnt += child.repositories.count()
2858 2863 cnt += children_count(child)
2859 2864 return cnt
2860 2865
2861 2866 return cnt + children_count(self)
2862 2867
2863 2868 def _recursive_objects(self, include_repos=True, include_groups=True):
2864 2869 all_ = []
2865 2870
2866 2871 def _get_members(root_gr):
2867 2872 if include_repos:
2868 2873 for r in root_gr.repositories:
2869 2874 all_.append(r)
2870 2875 childs = root_gr.children.all()
2871 2876 if childs:
2872 2877 for gr in childs:
2873 2878 if include_groups:
2874 2879 all_.append(gr)
2875 2880 _get_members(gr)
2876 2881
2877 2882 root_group = []
2878 2883 if include_groups:
2879 2884 root_group = [self]
2880 2885
2881 2886 _get_members(self)
2882 2887 return root_group + all_
2883 2888
2884 2889 def recursive_groups_and_repos(self):
2885 2890 """
2886 2891 Recursive return all groups, with repositories in those groups
2887 2892 """
2888 2893 return self._recursive_objects()
2889 2894
2890 2895 def recursive_groups(self):
2891 2896 """
2892 2897 Returns all children groups for this group including children of children
2893 2898 """
2894 2899 return self._recursive_objects(include_repos=False)
2895 2900
2896 2901 def recursive_repos(self):
2897 2902 """
2898 2903 Returns all children repositories for this group
2899 2904 """
2900 2905 return self._recursive_objects(include_groups=False)
2901 2906
2902 2907 def get_new_name(self, group_name):
2903 2908 """
2904 2909 returns new full group name based on parent and new name
2905 2910
2906 2911 :param group_name:
2907 2912 """
2908 2913 path_prefix = (self.parent_group.full_path_splitted if
2909 2914 self.parent_group else [])
2910 2915 return RepoGroup.url_sep().join(path_prefix + [group_name])
2911 2916
2912 2917 def update_commit_cache(self, config=None):
2913 2918 """
2914 2919 Update cache of last commit for newest repository inside this repository group.
2915 2920 cache_keys should be::
2916 2921
2917 2922 source_repo_id
2918 2923 short_id
2919 2924 raw_id
2920 2925 revision
2921 2926 parents
2922 2927 message
2923 2928 date
2924 2929 author
2925 2930
2926 2931 """
2927 2932 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2928 2933 empty_date = datetime.datetime.fromtimestamp(0)
2929 2934
2930 2935 def repo_groups_and_repos(root_gr):
2931 2936 for _repo in root_gr.repositories:
2932 2937 yield _repo
2933 2938 for child_group in root_gr.children.all():
2934 2939 yield child_group
2935 2940
2936 2941 latest_repo_cs_cache = {}
2937 2942 for obj in repo_groups_and_repos(self):
2938 2943 repo_cs_cache = obj.changeset_cache
2939 2944 date_latest = latest_repo_cs_cache.get('date', empty_date)
2940 2945 date_current = repo_cs_cache.get('date', empty_date)
2941 2946 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2942 2947 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2943 2948 latest_repo_cs_cache = repo_cs_cache
2944 2949 if hasattr(obj, 'repo_id'):
2945 2950 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2946 2951 else:
2947 2952 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2948 2953
2949 2954 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2950 2955
2951 2956 latest_repo_cs_cache['updated_on'] = time.time()
2952 2957 self.changeset_cache = latest_repo_cs_cache
2953 2958 self.updated_on = _date_latest
2954 2959 Session().add(self)
2955 2960 Session().commit()
2956 2961
2957 2962 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2958 2963 self.group_name, latest_repo_cs_cache, _date_latest)
2959 2964
2960 2965 def permissions(self, with_admins=True, with_owner=True,
2961 2966 expand_from_user_groups=False):
2962 2967 """
2963 2968 Permissions for repository groups
2964 2969 """
2965 2970 _admin_perm = 'group.admin'
2966 2971
2967 2972 owner_row = []
2968 2973 if with_owner:
2969 2974 usr = AttributeDict(self.user.get_dict())
2970 2975 usr.owner_row = True
2971 2976 usr.permission = _admin_perm
2972 2977 owner_row.append(usr)
2973 2978
2974 2979 super_admin_ids = []
2975 2980 super_admin_rows = []
2976 2981 if with_admins:
2977 2982 for usr in User.get_all_super_admins():
2978 2983 super_admin_ids.append(usr.user_id)
2979 2984 # if this admin is also owner, don't double the record
2980 2985 if usr.user_id == owner_row[0].user_id:
2981 2986 owner_row[0].admin_row = True
2982 2987 else:
2983 2988 usr = AttributeDict(usr.get_dict())
2984 2989 usr.admin_row = True
2985 2990 usr.permission = _admin_perm
2986 2991 super_admin_rows.append(usr)
2987 2992
2988 2993 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2989 2994 q = q.options(joinedload(UserRepoGroupToPerm.group),
2990 2995 joinedload(UserRepoGroupToPerm.user),
2991 2996 joinedload(UserRepoGroupToPerm.permission),)
2992 2997
2993 2998 # get owners and admins and permissions. We do a trick of re-writing
2994 2999 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2995 3000 # has a global reference and changing one object propagates to all
2996 3001 # others. This means if admin is also an owner admin_row that change
2997 3002 # would propagate to both objects
2998 3003 perm_rows = []
2999 3004 for _usr in q.all():
3000 3005 usr = AttributeDict(_usr.user.get_dict())
3001 3006 # if this user is also owner/admin, mark as duplicate record
3002 3007 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3003 3008 usr.duplicate_perm = True
3004 3009 usr.permission = _usr.permission.permission_name
3005 3010 perm_rows.append(usr)
3006 3011
3007 3012 # filter the perm rows by 'default' first and then sort them by
3008 3013 # admin,write,read,none permissions sorted again alphabetically in
3009 3014 # each group
3010 3015 perm_rows = sorted(perm_rows, key=display_user_sort)
3011 3016
3012 3017 user_groups_rows = []
3013 3018 if expand_from_user_groups:
3014 3019 for ug in self.permission_user_groups(with_members=True):
3015 3020 for user_data in ug.members:
3016 3021 user_groups_rows.append(user_data)
3017 3022
3018 3023 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3019 3024
3020 3025 def permission_user_groups(self, with_members=False):
3021 3026 q = UserGroupRepoGroupToPerm.query()\
3022 3027 .filter(UserGroupRepoGroupToPerm.group == self)
3023 3028 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3024 3029 joinedload(UserGroupRepoGroupToPerm.users_group),
3025 3030 joinedload(UserGroupRepoGroupToPerm.permission),)
3026 3031
3027 3032 perm_rows = []
3028 3033 for _user_group in q.all():
3029 3034 entry = AttributeDict(_user_group.users_group.get_dict())
3030 3035 entry.permission = _user_group.permission.permission_name
3031 3036 if with_members:
3032 3037 entry.members = [x.user.get_dict()
3033 3038 for x in _user_group.users_group.members]
3034 3039 perm_rows.append(entry)
3035 3040
3036 3041 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3037 3042 return perm_rows
3038 3043
3039 3044 def get_api_data(self):
3040 3045 """
3041 3046 Common function for generating api data
3042 3047
3043 3048 """
3044 3049 group = self
3045 3050 data = {
3046 3051 'group_id': group.group_id,
3047 3052 'group_name': group.group_name,
3048 3053 'group_description': group.description_safe,
3049 3054 'parent_group': group.parent_group.group_name if group.parent_group else None,
3050 3055 'repositories': [x.repo_name for x in group.repositories],
3051 3056 'owner': group.user.username,
3052 3057 }
3053 3058 return data
3054 3059
3055 3060 def get_dict(self):
3056 3061 # Since we transformed `group_name` to a hybrid property, we need to
3057 3062 # keep compatibility with the code which uses `group_name` field.
3058 3063 result = super(RepoGroup, self).get_dict()
3059 3064 result['group_name'] = result.pop('_group_name', None)
3060 3065 return result
3061 3066
3062 3067
3063 3068 class Permission(Base, BaseModel):
3064 3069 __tablename__ = 'permissions'
3065 3070 __table_args__ = (
3066 3071 Index('p_perm_name_idx', 'permission_name'),
3067 3072 base_table_args,
3068 3073 )
3069 3074
3070 3075 PERMS = [
3071 3076 ('hg.admin', _('RhodeCode Super Administrator')),
3072 3077
3073 3078 ('repository.none', _('Repository no access')),
3074 3079 ('repository.read', _('Repository read access')),
3075 3080 ('repository.write', _('Repository write access')),
3076 3081 ('repository.admin', _('Repository admin access')),
3077 3082
3078 3083 ('group.none', _('Repository group no access')),
3079 3084 ('group.read', _('Repository group read access')),
3080 3085 ('group.write', _('Repository group write access')),
3081 3086 ('group.admin', _('Repository group admin access')),
3082 3087
3083 3088 ('usergroup.none', _('User group no access')),
3084 3089 ('usergroup.read', _('User group read access')),
3085 3090 ('usergroup.write', _('User group write access')),
3086 3091 ('usergroup.admin', _('User group admin access')),
3087 3092
3088 3093 ('branch.none', _('Branch no permissions')),
3089 3094 ('branch.merge', _('Branch access by web merge')),
3090 3095 ('branch.push', _('Branch access by push')),
3091 3096 ('branch.push_force', _('Branch access by push with force')),
3092 3097
3093 3098 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3094 3099 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3095 3100
3096 3101 ('hg.usergroup.create.false', _('User Group creation disabled')),
3097 3102 ('hg.usergroup.create.true', _('User Group creation enabled')),
3098 3103
3099 3104 ('hg.create.none', _('Repository creation disabled')),
3100 3105 ('hg.create.repository', _('Repository creation enabled')),
3101 3106 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3102 3107 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3103 3108
3104 3109 ('hg.fork.none', _('Repository forking disabled')),
3105 3110 ('hg.fork.repository', _('Repository forking enabled')),
3106 3111
3107 3112 ('hg.register.none', _('Registration disabled')),
3108 3113 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3109 3114 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3110 3115
3111 3116 ('hg.password_reset.enabled', _('Password reset enabled')),
3112 3117 ('hg.password_reset.hidden', _('Password reset hidden')),
3113 3118 ('hg.password_reset.disabled', _('Password reset disabled')),
3114 3119
3115 3120 ('hg.extern_activate.manual', _('Manual activation of external account')),
3116 3121 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3117 3122
3118 3123 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3119 3124 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3120 3125 ]
3121 3126
3122 3127 # definition of system default permissions for DEFAULT user, created on
3123 3128 # system setup
3124 3129 DEFAULT_USER_PERMISSIONS = [
3125 3130 # object perms
3126 3131 'repository.read',
3127 3132 'group.read',
3128 3133 'usergroup.read',
3129 3134 # branch, for backward compat we need same value as before so forced pushed
3130 3135 'branch.push_force',
3131 3136 # global
3132 3137 'hg.create.repository',
3133 3138 'hg.repogroup.create.false',
3134 3139 'hg.usergroup.create.false',
3135 3140 'hg.create.write_on_repogroup.true',
3136 3141 'hg.fork.repository',
3137 3142 'hg.register.manual_activate',
3138 3143 'hg.password_reset.enabled',
3139 3144 'hg.extern_activate.auto',
3140 3145 'hg.inherit_default_perms.true',
3141 3146 ]
3142 3147
3143 3148 # defines which permissions are more important higher the more important
3144 3149 # Weight defines which permissions are more important.
3145 3150 # The higher number the more important.
3146 3151 PERM_WEIGHTS = {
3147 3152 'repository.none': 0,
3148 3153 'repository.read': 1,
3149 3154 'repository.write': 3,
3150 3155 'repository.admin': 4,
3151 3156
3152 3157 'group.none': 0,
3153 3158 'group.read': 1,
3154 3159 'group.write': 3,
3155 3160 'group.admin': 4,
3156 3161
3157 3162 'usergroup.none': 0,
3158 3163 'usergroup.read': 1,
3159 3164 'usergroup.write': 3,
3160 3165 'usergroup.admin': 4,
3161 3166
3162 3167 'branch.none': 0,
3163 3168 'branch.merge': 1,
3164 3169 'branch.push': 3,
3165 3170 'branch.push_force': 4,
3166 3171
3167 3172 'hg.repogroup.create.false': 0,
3168 3173 'hg.repogroup.create.true': 1,
3169 3174
3170 3175 'hg.usergroup.create.false': 0,
3171 3176 'hg.usergroup.create.true': 1,
3172 3177
3173 3178 'hg.fork.none': 0,
3174 3179 'hg.fork.repository': 1,
3175 3180 'hg.create.none': 0,
3176 3181 'hg.create.repository': 1
3177 3182 }
3178 3183
3179 3184 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3180 3185 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3181 3186 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3182 3187
3183 3188 def __unicode__(self):
3184 3189 return u"<%s('%s:%s')>" % (
3185 3190 self.__class__.__name__, self.permission_id, self.permission_name
3186 3191 )
3187 3192
3188 3193 @classmethod
3189 3194 def get_by_key(cls, key):
3190 3195 return cls.query().filter(cls.permission_name == key).scalar()
3191 3196
3192 3197 @classmethod
3193 3198 def get_default_repo_perms(cls, user_id, repo_id=None):
3194 3199 q = Session().query(UserRepoToPerm, Repository, Permission)\
3195 3200 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3196 3201 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3197 3202 .filter(UserRepoToPerm.user_id == user_id)
3198 3203 if repo_id:
3199 3204 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3200 3205 return q.all()
3201 3206
3202 3207 @classmethod
3203 3208 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3204 3209 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3205 3210 .join(
3206 3211 Permission,
3207 3212 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3208 3213 .join(
3209 3214 UserRepoToPerm,
3210 3215 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3211 3216 .filter(UserRepoToPerm.user_id == user_id)
3212 3217
3213 3218 if repo_id:
3214 3219 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3215 3220 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3216 3221
3217 3222 @classmethod
3218 3223 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3219 3224 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3220 3225 .join(
3221 3226 Permission,
3222 3227 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3223 3228 .join(
3224 3229 Repository,
3225 3230 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3226 3231 .join(
3227 3232 UserGroup,
3228 3233 UserGroupRepoToPerm.users_group_id ==
3229 3234 UserGroup.users_group_id)\
3230 3235 .join(
3231 3236 UserGroupMember,
3232 3237 UserGroupRepoToPerm.users_group_id ==
3233 3238 UserGroupMember.users_group_id)\
3234 3239 .filter(
3235 3240 UserGroupMember.user_id == user_id,
3236 3241 UserGroup.users_group_active == true())
3237 3242 if repo_id:
3238 3243 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3239 3244 return q.all()
3240 3245
3241 3246 @classmethod
3242 3247 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3243 3248 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3244 3249 .join(
3245 3250 Permission,
3246 3251 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3247 3252 .join(
3248 3253 UserGroupRepoToPerm,
3249 3254 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3250 3255 .join(
3251 3256 UserGroup,
3252 3257 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3253 3258 .join(
3254 3259 UserGroupMember,
3255 3260 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3256 3261 .filter(
3257 3262 UserGroupMember.user_id == user_id,
3258 3263 UserGroup.users_group_active == true())
3259 3264
3260 3265 if repo_id:
3261 3266 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3262 3267 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3263 3268
3264 3269 @classmethod
3265 3270 def get_default_group_perms(cls, user_id, repo_group_id=None):
3266 3271 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3267 3272 .join(
3268 3273 Permission,
3269 3274 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3270 3275 .join(
3271 3276 RepoGroup,
3272 3277 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3273 3278 .filter(UserRepoGroupToPerm.user_id == user_id)
3274 3279 if repo_group_id:
3275 3280 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3276 3281 return q.all()
3277 3282
3278 3283 @classmethod
3279 3284 def get_default_group_perms_from_user_group(
3280 3285 cls, user_id, repo_group_id=None):
3281 3286 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3282 3287 .join(
3283 3288 Permission,
3284 3289 UserGroupRepoGroupToPerm.permission_id ==
3285 3290 Permission.permission_id)\
3286 3291 .join(
3287 3292 RepoGroup,
3288 3293 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3289 3294 .join(
3290 3295 UserGroup,
3291 3296 UserGroupRepoGroupToPerm.users_group_id ==
3292 3297 UserGroup.users_group_id)\
3293 3298 .join(
3294 3299 UserGroupMember,
3295 3300 UserGroupRepoGroupToPerm.users_group_id ==
3296 3301 UserGroupMember.users_group_id)\
3297 3302 .filter(
3298 3303 UserGroupMember.user_id == user_id,
3299 3304 UserGroup.users_group_active == true())
3300 3305 if repo_group_id:
3301 3306 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3302 3307 return q.all()
3303 3308
3304 3309 @classmethod
3305 3310 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3306 3311 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3307 3312 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3308 3313 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3309 3314 .filter(UserUserGroupToPerm.user_id == user_id)
3310 3315 if user_group_id:
3311 3316 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3312 3317 return q.all()
3313 3318
3314 3319 @classmethod
3315 3320 def get_default_user_group_perms_from_user_group(
3316 3321 cls, user_id, user_group_id=None):
3317 3322 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3318 3323 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3319 3324 .join(
3320 3325 Permission,
3321 3326 UserGroupUserGroupToPerm.permission_id ==
3322 3327 Permission.permission_id)\
3323 3328 .join(
3324 3329 TargetUserGroup,
3325 3330 UserGroupUserGroupToPerm.target_user_group_id ==
3326 3331 TargetUserGroup.users_group_id)\
3327 3332 .join(
3328 3333 UserGroup,
3329 3334 UserGroupUserGroupToPerm.user_group_id ==
3330 3335 UserGroup.users_group_id)\
3331 3336 .join(
3332 3337 UserGroupMember,
3333 3338 UserGroupUserGroupToPerm.user_group_id ==
3334 3339 UserGroupMember.users_group_id)\
3335 3340 .filter(
3336 3341 UserGroupMember.user_id == user_id,
3337 3342 UserGroup.users_group_active == true())
3338 3343 if user_group_id:
3339 3344 q = q.filter(
3340 3345 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3341 3346
3342 3347 return q.all()
3343 3348
3344 3349
3345 3350 class UserRepoToPerm(Base, BaseModel):
3346 3351 __tablename__ = 'repo_to_perm'
3347 3352 __table_args__ = (
3348 3353 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3349 3354 base_table_args
3350 3355 )
3351 3356
3352 3357 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3353 3358 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3354 3359 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3355 3360 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3356 3361
3357 3362 user = relationship('User')
3358 3363 repository = relationship('Repository')
3359 3364 permission = relationship('Permission')
3360 3365
3361 3366 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3362 3367
3363 3368 @classmethod
3364 3369 def create(cls, user, repository, permission):
3365 3370 n = cls()
3366 3371 n.user = user
3367 3372 n.repository = repository
3368 3373 n.permission = permission
3369 3374 Session().add(n)
3370 3375 return n
3371 3376
3372 3377 def __unicode__(self):
3373 3378 return u'<%s => %s >' % (self.user, self.repository)
3374 3379
3375 3380
3376 3381 class UserUserGroupToPerm(Base, BaseModel):
3377 3382 __tablename__ = 'user_user_group_to_perm'
3378 3383 __table_args__ = (
3379 3384 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3380 3385 base_table_args
3381 3386 )
3382 3387
3383 3388 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3384 3389 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3385 3390 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3386 3391 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3387 3392
3388 3393 user = relationship('User')
3389 3394 user_group = relationship('UserGroup')
3390 3395 permission = relationship('Permission')
3391 3396
3392 3397 @classmethod
3393 3398 def create(cls, user, user_group, permission):
3394 3399 n = cls()
3395 3400 n.user = user
3396 3401 n.user_group = user_group
3397 3402 n.permission = permission
3398 3403 Session().add(n)
3399 3404 return n
3400 3405
3401 3406 def __unicode__(self):
3402 3407 return u'<%s => %s >' % (self.user, self.user_group)
3403 3408
3404 3409
3405 3410 class UserToPerm(Base, BaseModel):
3406 3411 __tablename__ = 'user_to_perm'
3407 3412 __table_args__ = (
3408 3413 UniqueConstraint('user_id', 'permission_id'),
3409 3414 base_table_args
3410 3415 )
3411 3416
3412 3417 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3413 3418 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3414 3419 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3415 3420
3416 3421 user = relationship('User')
3417 3422 permission = relationship('Permission', lazy='joined')
3418 3423
3419 3424 def __unicode__(self):
3420 3425 return u'<%s => %s >' % (self.user, self.permission)
3421 3426
3422 3427
3423 3428 class UserGroupRepoToPerm(Base, BaseModel):
3424 3429 __tablename__ = 'users_group_repo_to_perm'
3425 3430 __table_args__ = (
3426 3431 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3427 3432 base_table_args
3428 3433 )
3429 3434
3430 3435 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3431 3436 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3432 3437 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3433 3438 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3434 3439
3435 3440 users_group = relationship('UserGroup')
3436 3441 permission = relationship('Permission')
3437 3442 repository = relationship('Repository')
3438 3443 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3439 3444
3440 3445 @classmethod
3441 3446 def create(cls, users_group, repository, permission):
3442 3447 n = cls()
3443 3448 n.users_group = users_group
3444 3449 n.repository = repository
3445 3450 n.permission = permission
3446 3451 Session().add(n)
3447 3452 return n
3448 3453
3449 3454 def __unicode__(self):
3450 3455 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3451 3456
3452 3457
3453 3458 class UserGroupUserGroupToPerm(Base, BaseModel):
3454 3459 __tablename__ = 'user_group_user_group_to_perm'
3455 3460 __table_args__ = (
3456 3461 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3457 3462 CheckConstraint('target_user_group_id != user_group_id'),
3458 3463 base_table_args
3459 3464 )
3460 3465
3461 3466 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)
3462 3467 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3463 3468 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3464 3469 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3465 3470
3466 3471 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3467 3472 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3468 3473 permission = relationship('Permission')
3469 3474
3470 3475 @classmethod
3471 3476 def create(cls, target_user_group, user_group, permission):
3472 3477 n = cls()
3473 3478 n.target_user_group = target_user_group
3474 3479 n.user_group = user_group
3475 3480 n.permission = permission
3476 3481 Session().add(n)
3477 3482 return n
3478 3483
3479 3484 def __unicode__(self):
3480 3485 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3481 3486
3482 3487
3483 3488 class UserGroupToPerm(Base, BaseModel):
3484 3489 __tablename__ = 'users_group_to_perm'
3485 3490 __table_args__ = (
3486 3491 UniqueConstraint('users_group_id', 'permission_id',),
3487 3492 base_table_args
3488 3493 )
3489 3494
3490 3495 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3491 3496 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3492 3497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3493 3498
3494 3499 users_group = relationship('UserGroup')
3495 3500 permission = relationship('Permission')
3496 3501
3497 3502
3498 3503 class UserRepoGroupToPerm(Base, BaseModel):
3499 3504 __tablename__ = 'user_repo_group_to_perm'
3500 3505 __table_args__ = (
3501 3506 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3502 3507 base_table_args
3503 3508 )
3504 3509
3505 3510 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3506 3511 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3507 3512 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3508 3513 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3509 3514
3510 3515 user = relationship('User')
3511 3516 group = relationship('RepoGroup')
3512 3517 permission = relationship('Permission')
3513 3518
3514 3519 @classmethod
3515 3520 def create(cls, user, repository_group, permission):
3516 3521 n = cls()
3517 3522 n.user = user
3518 3523 n.group = repository_group
3519 3524 n.permission = permission
3520 3525 Session().add(n)
3521 3526 return n
3522 3527
3523 3528
3524 3529 class UserGroupRepoGroupToPerm(Base, BaseModel):
3525 3530 __tablename__ = 'users_group_repo_group_to_perm'
3526 3531 __table_args__ = (
3527 3532 UniqueConstraint('users_group_id', 'group_id'),
3528 3533 base_table_args
3529 3534 )
3530 3535
3531 3536 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)
3532 3537 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3533 3538 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3534 3539 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3535 3540
3536 3541 users_group = relationship('UserGroup')
3537 3542 permission = relationship('Permission')
3538 3543 group = relationship('RepoGroup')
3539 3544
3540 3545 @classmethod
3541 3546 def create(cls, user_group, repository_group, permission):
3542 3547 n = cls()
3543 3548 n.users_group = user_group
3544 3549 n.group = repository_group
3545 3550 n.permission = permission
3546 3551 Session().add(n)
3547 3552 return n
3548 3553
3549 3554 def __unicode__(self):
3550 3555 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3551 3556
3552 3557
3553 3558 class Statistics(Base, BaseModel):
3554 3559 __tablename__ = 'statistics'
3555 3560 __table_args__ = (
3556 3561 base_table_args
3557 3562 )
3558 3563
3559 3564 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3560 3565 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3561 3566 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3562 3567 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3563 3568 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3564 3569 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3565 3570
3566 3571 repository = relationship('Repository', single_parent=True)
3567 3572
3568 3573
3569 3574 class UserFollowing(Base, BaseModel):
3570 3575 __tablename__ = 'user_followings'
3571 3576 __table_args__ = (
3572 3577 UniqueConstraint('user_id', 'follows_repository_id'),
3573 3578 UniqueConstraint('user_id', 'follows_user_id'),
3574 3579 base_table_args
3575 3580 )
3576 3581
3577 3582 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3578 3583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3579 3584 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3580 3585 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3581 3586 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3582 3587
3583 3588 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3584 3589
3585 3590 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3586 3591 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3587 3592
3588 3593 @classmethod
3589 3594 def get_repo_followers(cls, repo_id):
3590 3595 return cls.query().filter(cls.follows_repo_id == repo_id)
3591 3596
3592 3597
3593 3598 class CacheKey(Base, BaseModel):
3594 3599 __tablename__ = 'cache_invalidation'
3595 3600 __table_args__ = (
3596 3601 UniqueConstraint('cache_key'),
3597 3602 Index('key_idx', 'cache_key'),
3598 3603 base_table_args,
3599 3604 )
3600 3605
3601 3606 CACHE_TYPE_FEED = 'FEED'
3602 3607
3603 3608 # namespaces used to register process/thread aware caches
3604 3609 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3605 3610 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3606 3611
3607 3612 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3608 3613 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3609 3614 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3610 3615 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3611 3616 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3612 3617
3613 3618 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3614 3619 self.cache_key = cache_key
3615 3620 self.cache_args = cache_args
3616 3621 self.cache_active = False
3617 3622 # first key should be same for all entries, since all workers should share it
3618 3623 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3619 3624
3620 3625 def __unicode__(self):
3621 3626 return u"<%s('%s:%s[%s]')>" % (
3622 3627 self.__class__.__name__,
3623 3628 self.cache_id, self.cache_key, self.cache_active)
3624 3629
3625 3630 def _cache_key_partition(self):
3626 3631 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3627 3632 return prefix, repo_name, suffix
3628 3633
3629 3634 def get_prefix(self):
3630 3635 """
3631 3636 Try to extract prefix from existing cache key. The key could consist
3632 3637 of prefix, repo_name, suffix
3633 3638 """
3634 3639 # this returns prefix, repo_name, suffix
3635 3640 return self._cache_key_partition()[0]
3636 3641
3637 3642 def get_suffix(self):
3638 3643 """
3639 3644 get suffix that might have been used in _get_cache_key to
3640 3645 generate self.cache_key. Only used for informational purposes
3641 3646 in repo_edit.mako.
3642 3647 """
3643 3648 # prefix, repo_name, suffix
3644 3649 return self._cache_key_partition()[2]
3645 3650
3646 3651 @classmethod
3647 3652 def generate_new_state_uid(cls, based_on=None):
3648 3653 if based_on:
3649 3654 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3650 3655 else:
3651 3656 return str(uuid.uuid4())
3652 3657
3653 3658 @classmethod
3654 3659 def delete_all_cache(cls):
3655 3660 """
3656 3661 Delete all cache keys from database.
3657 3662 Should only be run when all instances are down and all entries
3658 3663 thus stale.
3659 3664 """
3660 3665 cls.query().delete()
3661 3666 Session().commit()
3662 3667
3663 3668 @classmethod
3664 3669 def set_invalidate(cls, cache_uid, delete=False):
3665 3670 """
3666 3671 Mark all caches of a repo as invalid in the database.
3667 3672 """
3668 3673
3669 3674 try:
3670 3675 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3671 3676 if delete:
3672 3677 qry.delete()
3673 3678 log.debug('cache objects deleted for cache args %s',
3674 3679 safe_str(cache_uid))
3675 3680 else:
3676 3681 qry.update({"cache_active": False,
3677 3682 "cache_state_uid": cls.generate_new_state_uid()})
3678 3683 log.debug('cache objects marked as invalid for cache args %s',
3679 3684 safe_str(cache_uid))
3680 3685
3681 3686 Session().commit()
3682 3687 except Exception:
3683 3688 log.exception(
3684 3689 'Cache key invalidation failed for cache args %s',
3685 3690 safe_str(cache_uid))
3686 3691 Session().rollback()
3687 3692
3688 3693 @classmethod
3689 3694 def get_active_cache(cls, cache_key):
3690 3695 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3691 3696 if inv_obj:
3692 3697 return inv_obj
3693 3698 return None
3694 3699
3695 3700 @classmethod
3696 3701 def get_namespace_map(cls, namespace):
3697 3702 return {
3698 3703 x.cache_key: x
3699 3704 for x in cls.query().filter(cls.cache_args == namespace)}
3700 3705
3701 3706
3702 3707 class ChangesetComment(Base, BaseModel):
3703 3708 __tablename__ = 'changeset_comments'
3704 3709 __table_args__ = (
3705 3710 Index('cc_revision_idx', 'revision'),
3706 3711 base_table_args,
3707 3712 )
3708 3713
3709 3714 COMMENT_OUTDATED = u'comment_outdated'
3710 3715 COMMENT_TYPE_NOTE = u'note'
3711 3716 COMMENT_TYPE_TODO = u'todo'
3712 3717 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3713 3718
3714 3719 OP_IMMUTABLE = u'immutable'
3715 3720 OP_CHANGEABLE = u'changeable'
3716 3721
3717 3722 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3718 3723 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3719 3724 revision = Column('revision', String(40), nullable=True)
3720 3725 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3721 3726 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3722 3727 line_no = Column('line_no', Unicode(10), nullable=True)
3723 3728 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3724 3729 f_path = Column('f_path', Unicode(1000), nullable=True)
3725 3730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3726 3731 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3727 3732 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3728 3733 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3729 3734 renderer = Column('renderer', Unicode(64), nullable=True)
3730 3735 display_state = Column('display_state', Unicode(128), nullable=True)
3731 3736 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3732 3737
3733 3738 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3734 3739 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3735 3740
3736 3741 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3737 3742 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3738 3743
3739 3744 author = relationship('User', lazy='joined')
3740 3745 repo = relationship('Repository')
3741 3746 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3742 3747 pull_request = relationship('PullRequest', lazy='joined')
3743 3748 pull_request_version = relationship('PullRequestVersion')
3744 3749
3745 3750 @classmethod
3746 3751 def get_users(cls, revision=None, pull_request_id=None):
3747 3752 """
3748 3753 Returns user associated with this ChangesetComment. ie those
3749 3754 who actually commented
3750 3755
3751 3756 :param cls:
3752 3757 :param revision:
3753 3758 """
3754 3759 q = Session().query(User)\
3755 3760 .join(ChangesetComment.author)
3756 3761 if revision:
3757 3762 q = q.filter(cls.revision == revision)
3758 3763 elif pull_request_id:
3759 3764 q = q.filter(cls.pull_request_id == pull_request_id)
3760 3765 return q.all()
3761 3766
3762 3767 @classmethod
3763 3768 def get_index_from_version(cls, pr_version, versions):
3764 3769 num_versions = [x.pull_request_version_id for x in versions]
3765 3770 try:
3766 3771 return num_versions.index(pr_version) +1
3767 3772 except (IndexError, ValueError):
3768 3773 return
3769 3774
3770 3775 @property
3771 3776 def outdated(self):
3772 3777 return self.display_state == self.COMMENT_OUTDATED
3773 3778
3774 3779 @property
3775 3780 def immutable(self):
3776 3781 return self.immutable_state == self.OP_IMMUTABLE
3777 3782
3778 3783 def outdated_at_version(self, version):
3779 3784 """
3780 3785 Checks if comment is outdated for given pull request version
3781 3786 """
3782 3787 return self.outdated and self.pull_request_version_id != version
3783 3788
3784 3789 def older_than_version(self, version):
3785 3790 """
3786 3791 Checks if comment is made from previous version than given
3787 3792 """
3788 3793 if version is None:
3789 3794 return self.pull_request_version_id is not None
3790 3795
3791 3796 return self.pull_request_version_id < version
3792 3797
3793 3798 @property
3794 3799 def resolved(self):
3795 3800 return self.resolved_by[0] if self.resolved_by else None
3796 3801
3797 3802 @property
3798 3803 def is_todo(self):
3799 3804 return self.comment_type == self.COMMENT_TYPE_TODO
3800 3805
3801 3806 @property
3802 3807 def is_inline(self):
3803 3808 return self.line_no and self.f_path
3804 3809
3805 3810 def get_index_version(self, versions):
3806 3811 return self.get_index_from_version(
3807 3812 self.pull_request_version_id, versions)
3808 3813
3809 3814 def __repr__(self):
3810 3815 if self.comment_id:
3811 3816 return '<DB:Comment #%s>' % self.comment_id
3812 3817 else:
3813 3818 return '<DB:Comment at %#x>' % id(self)
3814 3819
3815 3820 def get_api_data(self):
3816 3821 comment = self
3817 3822 data = {
3818 3823 'comment_id': comment.comment_id,
3819 3824 'comment_type': comment.comment_type,
3820 3825 'comment_text': comment.text,
3821 3826 'comment_status': comment.status_change,
3822 3827 'comment_f_path': comment.f_path,
3823 3828 'comment_lineno': comment.line_no,
3824 3829 'comment_author': comment.author,
3825 3830 'comment_created_on': comment.created_on,
3826 3831 'comment_resolved_by': self.resolved,
3827 3832 'comment_commit_id': comment.revision,
3828 3833 'comment_pull_request_id': comment.pull_request_id,
3829 3834 }
3830 3835 return data
3831 3836
3832 3837 def __json__(self):
3833 3838 data = dict()
3834 3839 data.update(self.get_api_data())
3835 3840 return data
3836 3841
3837 3842
3838 3843 class ChangesetStatus(Base, BaseModel):
3839 3844 __tablename__ = 'changeset_statuses'
3840 3845 __table_args__ = (
3841 3846 Index('cs_revision_idx', 'revision'),
3842 3847 Index('cs_version_idx', 'version'),
3843 3848 UniqueConstraint('repo_id', 'revision', 'version'),
3844 3849 base_table_args
3845 3850 )
3846 3851
3847 3852 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3848 3853 STATUS_APPROVED = 'approved'
3849 3854 STATUS_REJECTED = 'rejected'
3850 3855 STATUS_UNDER_REVIEW = 'under_review'
3851 3856
3852 3857 STATUSES = [
3853 3858 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3854 3859 (STATUS_APPROVED, _("Approved")),
3855 3860 (STATUS_REJECTED, _("Rejected")),
3856 3861 (STATUS_UNDER_REVIEW, _("Under Review")),
3857 3862 ]
3858 3863
3859 3864 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3860 3865 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3861 3866 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3862 3867 revision = Column('revision', String(40), nullable=False)
3863 3868 status = Column('status', String(128), nullable=False, default=DEFAULT)
3864 3869 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3865 3870 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3866 3871 version = Column('version', Integer(), nullable=False, default=0)
3867 3872 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3868 3873
3869 3874 author = relationship('User', lazy='joined')
3870 3875 repo = relationship('Repository')
3871 3876 comment = relationship('ChangesetComment', lazy='joined')
3872 3877 pull_request = relationship('PullRequest', lazy='joined')
3873 3878
3874 3879 def __unicode__(self):
3875 3880 return u"<%s('%s[v%s]:%s')>" % (
3876 3881 self.__class__.__name__,
3877 3882 self.status, self.version, self.author
3878 3883 )
3879 3884
3880 3885 @classmethod
3881 3886 def get_status_lbl(cls, value):
3882 3887 return dict(cls.STATUSES).get(value)
3883 3888
3884 3889 @property
3885 3890 def status_lbl(self):
3886 3891 return ChangesetStatus.get_status_lbl(self.status)
3887 3892
3888 3893 def get_api_data(self):
3889 3894 status = self
3890 3895 data = {
3891 3896 'status_id': status.changeset_status_id,
3892 3897 'status': status.status,
3893 3898 }
3894 3899 return data
3895 3900
3896 3901 def __json__(self):
3897 3902 data = dict()
3898 3903 data.update(self.get_api_data())
3899 3904 return data
3900 3905
3901 3906
3902 3907 class _SetState(object):
3903 3908 """
3904 3909 Context processor allowing changing state for sensitive operation such as
3905 3910 pull request update or merge
3906 3911 """
3907 3912
3908 3913 def __init__(self, pull_request, pr_state, back_state=None):
3909 3914 self._pr = pull_request
3910 3915 self._org_state = back_state or pull_request.pull_request_state
3911 3916 self._pr_state = pr_state
3912 3917 self._current_state = None
3913 3918
3914 3919 def __enter__(self):
3915 3920 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3916 3921 self._pr, self._pr_state)
3917 3922 self.set_pr_state(self._pr_state)
3918 3923 return self
3919 3924
3920 3925 def __exit__(self, exc_type, exc_val, exc_tb):
3921 3926 if exc_val is not None:
3922 3927 log.error(traceback.format_exc(exc_tb))
3923 3928 return None
3924 3929
3925 3930 self.set_pr_state(self._org_state)
3926 3931 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3927 3932 self._pr, self._org_state)
3928 3933
3929 3934 @property
3930 3935 def state(self):
3931 3936 return self._current_state
3932 3937
3933 3938 def set_pr_state(self, pr_state):
3934 3939 try:
3935 3940 self._pr.pull_request_state = pr_state
3936 3941 Session().add(self._pr)
3937 3942 Session().commit()
3938 3943 self._current_state = pr_state
3939 3944 except Exception:
3940 3945 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3941 3946 raise
3942 3947
3943 3948
3944 3949 class _PullRequestBase(BaseModel):
3945 3950 """
3946 3951 Common attributes of pull request and version entries.
3947 3952 """
3948 3953
3949 3954 # .status values
3950 3955 STATUS_NEW = u'new'
3951 3956 STATUS_OPEN = u'open'
3952 3957 STATUS_CLOSED = u'closed'
3953 3958
3954 3959 # available states
3955 3960 STATE_CREATING = u'creating'
3956 3961 STATE_UPDATING = u'updating'
3957 3962 STATE_MERGING = u'merging'
3958 3963 STATE_CREATED = u'created'
3959 3964
3960 3965 title = Column('title', Unicode(255), nullable=True)
3961 3966 description = Column(
3962 3967 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3963 3968 nullable=True)
3964 3969 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3965 3970
3966 3971 # new/open/closed status of pull request (not approve/reject/etc)
3967 3972 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3968 3973 created_on = Column(
3969 3974 'created_on', DateTime(timezone=False), nullable=False,
3970 3975 default=datetime.datetime.now)
3971 3976 updated_on = Column(
3972 3977 'updated_on', DateTime(timezone=False), nullable=False,
3973 3978 default=datetime.datetime.now)
3974 3979
3975 3980 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3976 3981
3977 3982 @declared_attr
3978 3983 def user_id(cls):
3979 3984 return Column(
3980 3985 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3981 3986 unique=None)
3982 3987
3983 3988 # 500 revisions max
3984 3989 _revisions = Column(
3985 3990 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3986 3991
3987 3992 @declared_attr
3988 3993 def source_repo_id(cls):
3989 3994 # TODO: dan: rename column to source_repo_id
3990 3995 return Column(
3991 3996 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3992 3997 nullable=False)
3993 3998
3994 3999 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3995 4000
3996 4001 @hybrid_property
3997 4002 def source_ref(self):
3998 4003 return self._source_ref
3999 4004
4000 4005 @source_ref.setter
4001 4006 def source_ref(self, val):
4002 4007 parts = (val or '').split(':')
4003 4008 if len(parts) != 3:
4004 4009 raise ValueError(
4005 4010 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4006 4011 self._source_ref = safe_unicode(val)
4007 4012
4008 4013 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4009 4014
4010 4015 @hybrid_property
4011 4016 def target_ref(self):
4012 4017 return self._target_ref
4013 4018
4014 4019 @target_ref.setter
4015 4020 def target_ref(self, val):
4016 4021 parts = (val or '').split(':')
4017 4022 if len(parts) != 3:
4018 4023 raise ValueError(
4019 4024 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4020 4025 self._target_ref = safe_unicode(val)
4021 4026
4022 4027 @declared_attr
4023 4028 def target_repo_id(cls):
4024 4029 # TODO: dan: rename column to target_repo_id
4025 4030 return Column(
4026 4031 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4027 4032 nullable=False)
4028 4033
4029 4034 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4030 4035
4031 4036 # TODO: dan: rename column to last_merge_source_rev
4032 4037 _last_merge_source_rev = Column(
4033 4038 'last_merge_org_rev', String(40), nullable=True)
4034 4039 # TODO: dan: rename column to last_merge_target_rev
4035 4040 _last_merge_target_rev = Column(
4036 4041 'last_merge_other_rev', String(40), nullable=True)
4037 4042 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4038 4043 last_merge_metadata = Column(
4039 4044 'last_merge_metadata', MutationObj.as_mutable(
4040 4045 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4041 4046
4042 4047 merge_rev = Column('merge_rev', String(40), nullable=True)
4043 4048
4044 4049 reviewer_data = Column(
4045 4050 'reviewer_data_json', MutationObj.as_mutable(
4046 4051 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4047 4052
4048 4053 @property
4049 4054 def reviewer_data_json(self):
4050 4055 return json.dumps(self.reviewer_data)
4051 4056
4052 4057 @property
4053 4058 def work_in_progress(self):
4054 4059 """checks if pull request is work in progress by checking the title"""
4055 4060 title = self.title.upper()
4056 4061 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4057 4062 return True
4058 4063 return False
4059 4064
4060 4065 @hybrid_property
4061 4066 def description_safe(self):
4062 4067 from rhodecode.lib import helpers as h
4063 4068 return h.escape(self.description)
4064 4069
4065 4070 @hybrid_property
4066 4071 def revisions(self):
4067 4072 return self._revisions.split(':') if self._revisions else []
4068 4073
4069 4074 @revisions.setter
4070 4075 def revisions(self, val):
4071 4076 self._revisions = u':'.join(val)
4072 4077
4073 4078 @hybrid_property
4074 4079 def last_merge_status(self):
4075 4080 return safe_int(self._last_merge_status)
4076 4081
4077 4082 @last_merge_status.setter
4078 4083 def last_merge_status(self, val):
4079 4084 self._last_merge_status = val
4080 4085
4081 4086 @declared_attr
4082 4087 def author(cls):
4083 4088 return relationship('User', lazy='joined')
4084 4089
4085 4090 @declared_attr
4086 4091 def source_repo(cls):
4087 4092 return relationship(
4088 4093 'Repository',
4089 4094 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4090 4095
4091 4096 @property
4092 4097 def source_ref_parts(self):
4093 4098 return self.unicode_to_reference(self.source_ref)
4094 4099
4095 4100 @declared_attr
4096 4101 def target_repo(cls):
4097 4102 return relationship(
4098 4103 'Repository',
4099 4104 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4100 4105
4101 4106 @property
4102 4107 def target_ref_parts(self):
4103 4108 return self.unicode_to_reference(self.target_ref)
4104 4109
4105 4110 @property
4106 4111 def shadow_merge_ref(self):
4107 4112 return self.unicode_to_reference(self._shadow_merge_ref)
4108 4113
4109 4114 @shadow_merge_ref.setter
4110 4115 def shadow_merge_ref(self, ref):
4111 4116 self._shadow_merge_ref = self.reference_to_unicode(ref)
4112 4117
4113 4118 @staticmethod
4114 4119 def unicode_to_reference(raw):
4115 4120 """
4116 4121 Convert a unicode (or string) to a reference object.
4117 4122 If unicode evaluates to False it returns None.
4118 4123 """
4119 4124 if raw:
4120 4125 refs = raw.split(':')
4121 4126 return Reference(*refs)
4122 4127 else:
4123 4128 return None
4124 4129
4125 4130 @staticmethod
4126 4131 def reference_to_unicode(ref):
4127 4132 """
4128 4133 Convert a reference object to unicode.
4129 4134 If reference is None it returns None.
4130 4135 """
4131 4136 if ref:
4132 4137 return u':'.join(ref)
4133 4138 else:
4134 4139 return None
4135 4140
4136 4141 def get_api_data(self, with_merge_state=True):
4137 4142 from rhodecode.model.pull_request import PullRequestModel
4138 4143
4139 4144 pull_request = self
4140 4145 if with_merge_state:
4141 4146 merge_response, merge_status, msg = \
4142 4147 PullRequestModel().merge_status(pull_request)
4143 4148 merge_state = {
4144 4149 'status': merge_status,
4145 4150 'message': safe_unicode(msg),
4146 4151 }
4147 4152 else:
4148 4153 merge_state = {'status': 'not_available',
4149 4154 'message': 'not_available'}
4150 4155
4151 4156 merge_data = {
4152 4157 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4153 4158 'reference': (
4154 4159 pull_request.shadow_merge_ref._asdict()
4155 4160 if pull_request.shadow_merge_ref else None),
4156 4161 }
4157 4162
4158 4163 data = {
4159 4164 'pull_request_id': pull_request.pull_request_id,
4160 4165 'url': PullRequestModel().get_url(pull_request),
4161 4166 'title': pull_request.title,
4162 4167 'description': pull_request.description,
4163 4168 'status': pull_request.status,
4164 4169 'state': pull_request.pull_request_state,
4165 4170 'created_on': pull_request.created_on,
4166 4171 'updated_on': pull_request.updated_on,
4167 4172 'commit_ids': pull_request.revisions,
4168 4173 'review_status': pull_request.calculated_review_status(),
4169 4174 'mergeable': merge_state,
4170 4175 'source': {
4171 4176 'clone_url': pull_request.source_repo.clone_url(),
4172 4177 'repository': pull_request.source_repo.repo_name,
4173 4178 'reference': {
4174 4179 'name': pull_request.source_ref_parts.name,
4175 4180 'type': pull_request.source_ref_parts.type,
4176 4181 'commit_id': pull_request.source_ref_parts.commit_id,
4177 4182 },
4178 4183 },
4179 4184 'target': {
4180 4185 'clone_url': pull_request.target_repo.clone_url(),
4181 4186 'repository': pull_request.target_repo.repo_name,
4182 4187 'reference': {
4183 4188 'name': pull_request.target_ref_parts.name,
4184 4189 'type': pull_request.target_ref_parts.type,
4185 4190 'commit_id': pull_request.target_ref_parts.commit_id,
4186 4191 },
4187 4192 },
4188 4193 'merge': merge_data,
4189 4194 'author': pull_request.author.get_api_data(include_secrets=False,
4190 4195 details='basic'),
4191 4196 'reviewers': [
4192 4197 {
4193 4198 'user': reviewer.get_api_data(include_secrets=False,
4194 4199 details='basic'),
4195 4200 'reasons': reasons,
4196 4201 'review_status': st[0][1].status if st else 'not_reviewed',
4197 4202 }
4198 4203 for obj, reviewer, reasons, mandatory, st in
4199 4204 pull_request.reviewers_statuses()
4200 4205 ]
4201 4206 }
4202 4207
4203 4208 return data
4204 4209
4205 4210 def set_state(self, pull_request_state, final_state=None):
4206 4211 """
4207 4212 # goes from initial state to updating to initial state.
4208 4213 # initial state can be changed by specifying back_state=
4209 4214 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4210 4215 pull_request.merge()
4211 4216
4212 4217 :param pull_request_state:
4213 4218 :param final_state:
4214 4219
4215 4220 """
4216 4221
4217 4222 return _SetState(self, pull_request_state, back_state=final_state)
4218 4223
4219 4224
4220 4225 class PullRequest(Base, _PullRequestBase):
4221 4226 __tablename__ = 'pull_requests'
4222 4227 __table_args__ = (
4223 4228 base_table_args,
4224 4229 )
4225 4230
4226 4231 pull_request_id = Column(
4227 4232 'pull_request_id', Integer(), nullable=False, primary_key=True)
4228 4233
4229 4234 def __repr__(self):
4230 4235 if self.pull_request_id:
4231 4236 return '<DB:PullRequest #%s>' % self.pull_request_id
4232 4237 else:
4233 4238 return '<DB:PullRequest at %#x>' % id(self)
4234 4239
4235 4240 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4236 4241 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4237 4242 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4238 4243 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4239 4244 lazy='dynamic')
4240 4245
4241 4246 @classmethod
4242 4247 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4243 4248 internal_methods=None):
4244 4249
4245 4250 class PullRequestDisplay(object):
4246 4251 """
4247 4252 Special object wrapper for showing PullRequest data via Versions
4248 4253 It mimics PR object as close as possible. This is read only object
4249 4254 just for display
4250 4255 """
4251 4256
4252 4257 def __init__(self, attrs, internal=None):
4253 4258 self.attrs = attrs
4254 4259 # internal have priority over the given ones via attrs
4255 4260 self.internal = internal or ['versions']
4256 4261
4257 4262 def __getattr__(self, item):
4258 4263 if item in self.internal:
4259 4264 return getattr(self, item)
4260 4265 try:
4261 4266 return self.attrs[item]
4262 4267 except KeyError:
4263 4268 raise AttributeError(
4264 4269 '%s object has no attribute %s' % (self, item))
4265 4270
4266 4271 def __repr__(self):
4267 4272 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4268 4273
4269 4274 def versions(self):
4270 4275 return pull_request_obj.versions.order_by(
4271 4276 PullRequestVersion.pull_request_version_id).all()
4272 4277
4273 4278 def is_closed(self):
4274 4279 return pull_request_obj.is_closed()
4275 4280
4276 4281 def is_state_changing(self):
4277 4282 return pull_request_obj.is_state_changing()
4278 4283
4279 4284 @property
4280 4285 def pull_request_version_id(self):
4281 4286 return getattr(pull_request_obj, 'pull_request_version_id', None)
4282 4287
4283 4288 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4284 4289
4285 4290 attrs.author = StrictAttributeDict(
4286 4291 pull_request_obj.author.get_api_data())
4287 4292 if pull_request_obj.target_repo:
4288 4293 attrs.target_repo = StrictAttributeDict(
4289 4294 pull_request_obj.target_repo.get_api_data())
4290 4295 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4291 4296
4292 4297 if pull_request_obj.source_repo:
4293 4298 attrs.source_repo = StrictAttributeDict(
4294 4299 pull_request_obj.source_repo.get_api_data())
4295 4300 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4296 4301
4297 4302 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4298 4303 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4299 4304 attrs.revisions = pull_request_obj.revisions
4300 4305
4301 4306 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4302 4307 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4303 4308 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4304 4309
4305 4310 return PullRequestDisplay(attrs, internal=internal_methods)
4306 4311
4307 4312 def is_closed(self):
4308 4313 return self.status == self.STATUS_CLOSED
4309 4314
4310 4315 def is_state_changing(self):
4311 4316 return self.pull_request_state != PullRequest.STATE_CREATED
4312 4317
4313 4318 def __json__(self):
4314 4319 return {
4315 4320 'revisions': self.revisions,
4316 4321 'versions': self.versions_count
4317 4322 }
4318 4323
4319 4324 def calculated_review_status(self):
4320 4325 from rhodecode.model.changeset_status import ChangesetStatusModel
4321 4326 return ChangesetStatusModel().calculated_review_status(self)
4322 4327
4323 4328 def reviewers_statuses(self):
4324 4329 from rhodecode.model.changeset_status import ChangesetStatusModel
4325 4330 return ChangesetStatusModel().reviewers_statuses(self)
4326 4331
4327 4332 @property
4328 4333 def workspace_id(self):
4329 4334 from rhodecode.model.pull_request import PullRequestModel
4330 4335 return PullRequestModel()._workspace_id(self)
4331 4336
4332 4337 def get_shadow_repo(self):
4333 4338 workspace_id = self.workspace_id
4334 4339 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4335 4340 if os.path.isdir(shadow_repository_path):
4336 4341 vcs_obj = self.target_repo.scm_instance()
4337 4342 return vcs_obj.get_shadow_instance(shadow_repository_path)
4338 4343
4339 4344 @property
4340 4345 def versions_count(self):
4341 4346 """
4342 4347 return number of versions this PR have, e.g a PR that once been
4343 4348 updated will have 2 versions
4344 4349 """
4345 4350 return self.versions.count() + 1
4346 4351
4347 4352
4348 4353 class PullRequestVersion(Base, _PullRequestBase):
4349 4354 __tablename__ = 'pull_request_versions'
4350 4355 __table_args__ = (
4351 4356 base_table_args,
4352 4357 )
4353 4358
4354 4359 pull_request_version_id = Column(
4355 4360 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4356 4361 pull_request_id = Column(
4357 4362 'pull_request_id', Integer(),
4358 4363 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4359 4364 pull_request = relationship('PullRequest')
4360 4365
4361 4366 def __repr__(self):
4362 4367 if self.pull_request_version_id:
4363 4368 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4364 4369 else:
4365 4370 return '<DB:PullRequestVersion at %#x>' % id(self)
4366 4371
4367 4372 @property
4368 4373 def reviewers(self):
4369 4374 return self.pull_request.reviewers
4370 4375
4371 4376 @property
4372 4377 def versions(self):
4373 4378 return self.pull_request.versions
4374 4379
4375 4380 def is_closed(self):
4376 4381 # calculate from original
4377 4382 return self.pull_request.status == self.STATUS_CLOSED
4378 4383
4379 4384 def is_state_changing(self):
4380 4385 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4381 4386
4382 4387 def calculated_review_status(self):
4383 4388 return self.pull_request.calculated_review_status()
4384 4389
4385 4390 def reviewers_statuses(self):
4386 4391 return self.pull_request.reviewers_statuses()
4387 4392
4388 4393
4389 4394 class PullRequestReviewers(Base, BaseModel):
4390 4395 __tablename__ = 'pull_request_reviewers'
4391 4396 __table_args__ = (
4392 4397 base_table_args,
4393 4398 )
4394 4399
4395 4400 @hybrid_property
4396 4401 def reasons(self):
4397 4402 if not self._reasons:
4398 4403 return []
4399 4404 return self._reasons
4400 4405
4401 4406 @reasons.setter
4402 4407 def reasons(self, val):
4403 4408 val = val or []
4404 4409 if any(not isinstance(x, compat.string_types) for x in val):
4405 4410 raise Exception('invalid reasons type, must be list of strings')
4406 4411 self._reasons = val
4407 4412
4408 4413 pull_requests_reviewers_id = Column(
4409 4414 'pull_requests_reviewers_id', Integer(), nullable=False,
4410 4415 primary_key=True)
4411 4416 pull_request_id = Column(
4412 4417 "pull_request_id", Integer(),
4413 4418 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4414 4419 user_id = Column(
4415 4420 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4416 4421 _reasons = Column(
4417 4422 'reason', MutationList.as_mutable(
4418 4423 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4419 4424
4420 4425 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4421 4426 user = relationship('User')
4422 4427 pull_request = relationship('PullRequest')
4423 4428
4424 4429 rule_data = Column(
4425 4430 'rule_data_json',
4426 4431 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4427 4432
4428 4433 def rule_user_group_data(self):
4429 4434 """
4430 4435 Returns the voting user group rule data for this reviewer
4431 4436 """
4432 4437
4433 4438 if self.rule_data and 'vote_rule' in self.rule_data:
4434 4439 user_group_data = {}
4435 4440 if 'rule_user_group_entry_id' in self.rule_data:
4436 4441 # means a group with voting rules !
4437 4442 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4438 4443 user_group_data['name'] = self.rule_data['rule_name']
4439 4444 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4440 4445
4441 4446 return user_group_data
4442 4447
4443 4448 def __unicode__(self):
4444 4449 return u"<%s('id:%s')>" % (self.__class__.__name__,
4445 4450 self.pull_requests_reviewers_id)
4446 4451
4447 4452
4448 4453 class Notification(Base, BaseModel):
4449 4454 __tablename__ = 'notifications'
4450 4455 __table_args__ = (
4451 4456 Index('notification_type_idx', 'type'),
4452 4457 base_table_args,
4453 4458 )
4454 4459
4455 4460 TYPE_CHANGESET_COMMENT = u'cs_comment'
4456 4461 TYPE_MESSAGE = u'message'
4457 4462 TYPE_MENTION = u'mention'
4458 4463 TYPE_REGISTRATION = u'registration'
4459 4464 TYPE_PULL_REQUEST = u'pull_request'
4460 4465 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4461 4466 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4462 4467
4463 4468 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4464 4469 subject = Column('subject', Unicode(512), nullable=True)
4465 4470 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4466 4471 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4467 4472 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4468 4473 type_ = Column('type', Unicode(255))
4469 4474
4470 4475 created_by_user = relationship('User')
4471 4476 notifications_to_users = relationship('UserNotification', lazy='joined',
4472 4477 cascade="all, delete-orphan")
4473 4478
4474 4479 @property
4475 4480 def recipients(self):
4476 4481 return [x.user for x in UserNotification.query()\
4477 4482 .filter(UserNotification.notification == self)\
4478 4483 .order_by(UserNotification.user_id.asc()).all()]
4479 4484
4480 4485 @classmethod
4481 4486 def create(cls, created_by, subject, body, recipients, type_=None):
4482 4487 if type_ is None:
4483 4488 type_ = Notification.TYPE_MESSAGE
4484 4489
4485 4490 notification = cls()
4486 4491 notification.created_by_user = created_by
4487 4492 notification.subject = subject
4488 4493 notification.body = body
4489 4494 notification.type_ = type_
4490 4495 notification.created_on = datetime.datetime.now()
4491 4496
4492 4497 # For each recipient link the created notification to his account
4493 4498 for u in recipients:
4494 4499 assoc = UserNotification()
4495 4500 assoc.user_id = u.user_id
4496 4501 assoc.notification = notification
4497 4502
4498 4503 # if created_by is inside recipients mark his notification
4499 4504 # as read
4500 4505 if u.user_id == created_by.user_id:
4501 4506 assoc.read = True
4502 4507 Session().add(assoc)
4503 4508
4504 4509 Session().add(notification)
4505 4510
4506 4511 return notification
4507 4512
4508 4513
4509 4514 class UserNotification(Base, BaseModel):
4510 4515 __tablename__ = 'user_to_notification'
4511 4516 __table_args__ = (
4512 4517 UniqueConstraint('user_id', 'notification_id'),
4513 4518 base_table_args
4514 4519 )
4515 4520
4516 4521 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4517 4522 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4518 4523 read = Column('read', Boolean, default=False)
4519 4524 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4520 4525
4521 4526 user = relationship('User', lazy="joined")
4522 4527 notification = relationship('Notification', lazy="joined",
4523 4528 order_by=lambda: Notification.created_on.desc(),)
4524 4529
4525 4530 def mark_as_read(self):
4526 4531 self.read = True
4527 4532 Session().add(self)
4528 4533
4529 4534
4530 4535 class UserNotice(Base, BaseModel):
4531 4536 __tablename__ = 'user_notices'
4532 4537 __table_args__ = (
4533 4538 base_table_args
4534 4539 )
4535 4540
4536 4541 NOTIFICATION_TYPE_MESSAGE = 'message'
4537 4542 NOTIFICATION_TYPE_NOTICE = 'notice'
4538 4543
4539 4544 NOTIFICATION_LEVEL_INFO = 'info'
4540 4545 NOTIFICATION_LEVEL_WARNING = 'warning'
4541 4546 NOTIFICATION_LEVEL_ERROR = 'error'
4542 4547
4543 4548 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4544 4549
4545 4550 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4546 4551 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4547 4552
4548 4553 notice_read = Column('notice_read', Boolean, default=False)
4549 4554
4550 4555 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4551 4556 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4552 4557
4553 4558 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4554 4559 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4555 4560
4556 4561 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4557 4562 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4558 4563
4559 4564 @classmethod
4560 4565 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4561 4566
4562 4567 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4563 4568 cls.NOTIFICATION_LEVEL_WARNING,
4564 4569 cls.NOTIFICATION_LEVEL_INFO]:
4565 4570 return
4566 4571
4567 4572 from rhodecode.model.user import UserModel
4568 4573 user = UserModel().get_user(user)
4569 4574
4570 4575 new_notice = UserNotice()
4571 4576 if not allow_duplicate:
4572 4577 existing_msg = UserNotice().query() \
4573 4578 .filter(UserNotice.user == user) \
4574 4579 .filter(UserNotice.notice_body == body) \
4575 4580 .filter(UserNotice.notice_read == false()) \
4576 4581 .scalar()
4577 4582 if existing_msg:
4578 4583 log.warning('Ignoring duplicate notice for user %s', user)
4579 4584 return
4580 4585
4581 4586 new_notice.user = user
4582 4587 new_notice.notice_subject = subject
4583 4588 new_notice.notice_body = body
4584 4589 new_notice.notification_level = notice_level
4585 4590 Session().add(new_notice)
4586 4591 Session().commit()
4587 4592
4588 4593
4589 4594 class Gist(Base, BaseModel):
4590 4595 __tablename__ = 'gists'
4591 4596 __table_args__ = (
4592 4597 Index('g_gist_access_id_idx', 'gist_access_id'),
4593 4598 Index('g_created_on_idx', 'created_on'),
4594 4599 base_table_args
4595 4600 )
4596 4601
4597 4602 GIST_PUBLIC = u'public'
4598 4603 GIST_PRIVATE = u'private'
4599 4604 DEFAULT_FILENAME = u'gistfile1.txt'
4600 4605
4601 4606 ACL_LEVEL_PUBLIC = u'acl_public'
4602 4607 ACL_LEVEL_PRIVATE = u'acl_private'
4603 4608
4604 4609 gist_id = Column('gist_id', Integer(), primary_key=True)
4605 4610 gist_access_id = Column('gist_access_id', Unicode(250))
4606 4611 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4607 4612 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4608 4613 gist_expires = Column('gist_expires', Float(53), nullable=False)
4609 4614 gist_type = Column('gist_type', Unicode(128), nullable=False)
4610 4615 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4611 4616 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4612 4617 acl_level = Column('acl_level', Unicode(128), nullable=True)
4613 4618
4614 4619 owner = relationship('User')
4615 4620
4616 4621 def __repr__(self):
4617 4622 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4618 4623
4619 4624 @hybrid_property
4620 4625 def description_safe(self):
4621 4626 from rhodecode.lib import helpers as h
4622 4627 return h.escape(self.gist_description)
4623 4628
4624 4629 @classmethod
4625 4630 def get_or_404(cls, id_):
4626 4631 from pyramid.httpexceptions import HTTPNotFound
4627 4632
4628 4633 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4629 4634 if not res:
4630 4635 raise HTTPNotFound()
4631 4636 return res
4632 4637
4633 4638 @classmethod
4634 4639 def get_by_access_id(cls, gist_access_id):
4635 4640 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4636 4641
4637 4642 def gist_url(self):
4638 4643 from rhodecode.model.gist import GistModel
4639 4644 return GistModel().get_url(self)
4640 4645
4641 4646 @classmethod
4642 4647 def base_path(cls):
4643 4648 """
4644 4649 Returns base path when all gists are stored
4645 4650
4646 4651 :param cls:
4647 4652 """
4648 4653 from rhodecode.model.gist import GIST_STORE_LOC
4649 4654 q = Session().query(RhodeCodeUi)\
4650 4655 .filter(RhodeCodeUi.ui_key == URL_SEP)
4651 4656 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4652 4657 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4653 4658
4654 4659 def get_api_data(self):
4655 4660 """
4656 4661 Common function for generating gist related data for API
4657 4662 """
4658 4663 gist = self
4659 4664 data = {
4660 4665 'gist_id': gist.gist_id,
4661 4666 'type': gist.gist_type,
4662 4667 'access_id': gist.gist_access_id,
4663 4668 'description': gist.gist_description,
4664 4669 'url': gist.gist_url(),
4665 4670 'expires': gist.gist_expires,
4666 4671 'created_on': gist.created_on,
4667 4672 'modified_at': gist.modified_at,
4668 4673 'content': None,
4669 4674 'acl_level': gist.acl_level,
4670 4675 }
4671 4676 return data
4672 4677
4673 4678 def __json__(self):
4674 4679 data = dict(
4675 4680 )
4676 4681 data.update(self.get_api_data())
4677 4682 return data
4678 4683 # SCM functions
4679 4684
4680 4685 def scm_instance(self, **kwargs):
4681 4686 """
4682 4687 Get an instance of VCS Repository
4683 4688
4684 4689 :param kwargs:
4685 4690 """
4686 4691 from rhodecode.model.gist import GistModel
4687 4692 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4688 4693 return get_vcs_instance(
4689 4694 repo_path=safe_str(full_repo_path), create=False,
4690 4695 _vcs_alias=GistModel.vcs_backend)
4691 4696
4692 4697
4693 4698 class ExternalIdentity(Base, BaseModel):
4694 4699 __tablename__ = 'external_identities'
4695 4700 __table_args__ = (
4696 4701 Index('local_user_id_idx', 'local_user_id'),
4697 4702 Index('external_id_idx', 'external_id'),
4698 4703 base_table_args
4699 4704 )
4700 4705
4701 4706 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4702 4707 external_username = Column('external_username', Unicode(1024), default=u'')
4703 4708 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4704 4709 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4705 4710 access_token = Column('access_token', String(1024), default=u'')
4706 4711 alt_token = Column('alt_token', String(1024), default=u'')
4707 4712 token_secret = Column('token_secret', String(1024), default=u'')
4708 4713
4709 4714 @classmethod
4710 4715 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4711 4716 """
4712 4717 Returns ExternalIdentity instance based on search params
4713 4718
4714 4719 :param external_id:
4715 4720 :param provider_name:
4716 4721 :return: ExternalIdentity
4717 4722 """
4718 4723 query = cls.query()
4719 4724 query = query.filter(cls.external_id == external_id)
4720 4725 query = query.filter(cls.provider_name == provider_name)
4721 4726 if local_user_id:
4722 4727 query = query.filter(cls.local_user_id == local_user_id)
4723 4728 return query.first()
4724 4729
4725 4730 @classmethod
4726 4731 def user_by_external_id_and_provider(cls, external_id, provider_name):
4727 4732 """
4728 4733 Returns User instance based on search params
4729 4734
4730 4735 :param external_id:
4731 4736 :param provider_name:
4732 4737 :return: User
4733 4738 """
4734 4739 query = User.query()
4735 4740 query = query.filter(cls.external_id == external_id)
4736 4741 query = query.filter(cls.provider_name == provider_name)
4737 4742 query = query.filter(User.user_id == cls.local_user_id)
4738 4743 return query.first()
4739 4744
4740 4745 @classmethod
4741 4746 def by_local_user_id(cls, local_user_id):
4742 4747 """
4743 4748 Returns all tokens for user
4744 4749
4745 4750 :param local_user_id:
4746 4751 :return: ExternalIdentity
4747 4752 """
4748 4753 query = cls.query()
4749 4754 query = query.filter(cls.local_user_id == local_user_id)
4750 4755 return query
4751 4756
4752 4757 @classmethod
4753 4758 def load_provider_plugin(cls, plugin_id):
4754 4759 from rhodecode.authentication.base import loadplugin
4755 4760 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4756 4761 auth_plugin = loadplugin(_plugin_id)
4757 4762 return auth_plugin
4758 4763
4759 4764
4760 4765 class Integration(Base, BaseModel):
4761 4766 __tablename__ = 'integrations'
4762 4767 __table_args__ = (
4763 4768 base_table_args
4764 4769 )
4765 4770
4766 4771 integration_id = Column('integration_id', Integer(), primary_key=True)
4767 4772 integration_type = Column('integration_type', String(255))
4768 4773 enabled = Column('enabled', Boolean(), nullable=False)
4769 4774 name = Column('name', String(255), nullable=False)
4770 4775 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4771 4776 default=False)
4772 4777
4773 4778 settings = Column(
4774 4779 'settings_json', MutationObj.as_mutable(
4775 4780 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4776 4781 repo_id = Column(
4777 4782 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4778 4783 nullable=True, unique=None, default=None)
4779 4784 repo = relationship('Repository', lazy='joined')
4780 4785
4781 4786 repo_group_id = Column(
4782 4787 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4783 4788 nullable=True, unique=None, default=None)
4784 4789 repo_group = relationship('RepoGroup', lazy='joined')
4785 4790
4786 4791 @property
4787 4792 def scope(self):
4788 4793 if self.repo:
4789 4794 return repr(self.repo)
4790 4795 if self.repo_group:
4791 4796 if self.child_repos_only:
4792 4797 return repr(self.repo_group) + ' (child repos only)'
4793 4798 else:
4794 4799 return repr(self.repo_group) + ' (recursive)'
4795 4800 if self.child_repos_only:
4796 4801 return 'root_repos'
4797 4802 return 'global'
4798 4803
4799 4804 def __repr__(self):
4800 4805 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4801 4806
4802 4807
4803 4808 class RepoReviewRuleUser(Base, BaseModel):
4804 4809 __tablename__ = 'repo_review_rules_users'
4805 4810 __table_args__ = (
4806 4811 base_table_args
4807 4812 )
4808 4813
4809 4814 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4810 4815 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4811 4816 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4812 4817 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4813 4818 user = relationship('User')
4814 4819
4815 4820 def rule_data(self):
4816 4821 return {
4817 4822 'mandatory': self.mandatory
4818 4823 }
4819 4824
4820 4825
4821 4826 class RepoReviewRuleUserGroup(Base, BaseModel):
4822 4827 __tablename__ = 'repo_review_rules_users_groups'
4823 4828 __table_args__ = (
4824 4829 base_table_args
4825 4830 )
4826 4831
4827 4832 VOTE_RULE_ALL = -1
4828 4833
4829 4834 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4830 4835 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4831 4836 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4832 4837 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4833 4838 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4834 4839 users_group = relationship('UserGroup')
4835 4840
4836 4841 def rule_data(self):
4837 4842 return {
4838 4843 'mandatory': self.mandatory,
4839 4844 'vote_rule': self.vote_rule
4840 4845 }
4841 4846
4842 4847 @property
4843 4848 def vote_rule_label(self):
4844 4849 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4845 4850 return 'all must vote'
4846 4851 else:
4847 4852 return 'min. vote {}'.format(self.vote_rule)
4848 4853
4849 4854
4850 4855 class RepoReviewRule(Base, BaseModel):
4851 4856 __tablename__ = 'repo_review_rules'
4852 4857 __table_args__ = (
4853 4858 base_table_args
4854 4859 )
4855 4860
4856 4861 repo_review_rule_id = Column(
4857 4862 'repo_review_rule_id', Integer(), primary_key=True)
4858 4863 repo_id = Column(
4859 4864 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4860 4865 repo = relationship('Repository', backref='review_rules')
4861 4866
4862 4867 review_rule_name = Column('review_rule_name', String(255))
4863 4868 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4864 4869 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4865 4870 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4866 4871
4867 4872 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4868 4873 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4869 4874 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4870 4875 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4871 4876
4872 4877 rule_users = relationship('RepoReviewRuleUser')
4873 4878 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4874 4879
4875 4880 def _validate_pattern(self, value):
4876 4881 re.compile('^' + glob2re(value) + '$')
4877 4882
4878 4883 @hybrid_property
4879 4884 def source_branch_pattern(self):
4880 4885 return self._branch_pattern or '*'
4881 4886
4882 4887 @source_branch_pattern.setter
4883 4888 def source_branch_pattern(self, value):
4884 4889 self._validate_pattern(value)
4885 4890 self._branch_pattern = value or '*'
4886 4891
4887 4892 @hybrid_property
4888 4893 def target_branch_pattern(self):
4889 4894 return self._target_branch_pattern or '*'
4890 4895
4891 4896 @target_branch_pattern.setter
4892 4897 def target_branch_pattern(self, value):
4893 4898 self._validate_pattern(value)
4894 4899 self._target_branch_pattern = value or '*'
4895 4900
4896 4901 @hybrid_property
4897 4902 def file_pattern(self):
4898 4903 return self._file_pattern or '*'
4899 4904
4900 4905 @file_pattern.setter
4901 4906 def file_pattern(self, value):
4902 4907 self._validate_pattern(value)
4903 4908 self._file_pattern = value or '*'
4904 4909
4905 4910 def matches(self, source_branch, target_branch, files_changed):
4906 4911 """
4907 4912 Check if this review rule matches a branch/files in a pull request
4908 4913
4909 4914 :param source_branch: source branch name for the commit
4910 4915 :param target_branch: target branch name for the commit
4911 4916 :param files_changed: list of file paths changed in the pull request
4912 4917 """
4913 4918
4914 4919 source_branch = source_branch or ''
4915 4920 target_branch = target_branch or ''
4916 4921 files_changed = files_changed or []
4917 4922
4918 4923 branch_matches = True
4919 4924 if source_branch or target_branch:
4920 4925 if self.source_branch_pattern == '*':
4921 4926 source_branch_match = True
4922 4927 else:
4923 4928 if self.source_branch_pattern.startswith('re:'):
4924 4929 source_pattern = self.source_branch_pattern[3:]
4925 4930 else:
4926 4931 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4927 4932 source_branch_regex = re.compile(source_pattern)
4928 4933 source_branch_match = bool(source_branch_regex.search(source_branch))
4929 4934 if self.target_branch_pattern == '*':
4930 4935 target_branch_match = True
4931 4936 else:
4932 4937 if self.target_branch_pattern.startswith('re:'):
4933 4938 target_pattern = self.target_branch_pattern[3:]
4934 4939 else:
4935 4940 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4936 4941 target_branch_regex = re.compile(target_pattern)
4937 4942 target_branch_match = bool(target_branch_regex.search(target_branch))
4938 4943
4939 4944 branch_matches = source_branch_match and target_branch_match
4940 4945
4941 4946 files_matches = True
4942 4947 if self.file_pattern != '*':
4943 4948 files_matches = False
4944 4949 if self.file_pattern.startswith('re:'):
4945 4950 file_pattern = self.file_pattern[3:]
4946 4951 else:
4947 4952 file_pattern = glob2re(self.file_pattern)
4948 4953 file_regex = re.compile(file_pattern)
4949 4954 for filename in files_changed:
4950 4955 if file_regex.search(filename):
4951 4956 files_matches = True
4952 4957 break
4953 4958
4954 4959 return branch_matches and files_matches
4955 4960
4956 4961 @property
4957 4962 def review_users(self):
4958 4963 """ Returns the users which this rule applies to """
4959 4964
4960 4965 users = collections.OrderedDict()
4961 4966
4962 4967 for rule_user in self.rule_users:
4963 4968 if rule_user.user.active:
4964 4969 if rule_user.user not in users:
4965 4970 users[rule_user.user.username] = {
4966 4971 'user': rule_user.user,
4967 4972 'source': 'user',
4968 4973 'source_data': {},
4969 4974 'data': rule_user.rule_data()
4970 4975 }
4971 4976
4972 4977 for rule_user_group in self.rule_user_groups:
4973 4978 source_data = {
4974 4979 'user_group_id': rule_user_group.users_group.users_group_id,
4975 4980 'name': rule_user_group.users_group.users_group_name,
4976 4981 'members': len(rule_user_group.users_group.members)
4977 4982 }
4978 4983 for member in rule_user_group.users_group.members:
4979 4984 if member.user.active:
4980 4985 key = member.user.username
4981 4986 if key in users:
4982 4987 # skip this member as we have him already
4983 4988 # this prevents from override the "first" matched
4984 4989 # users with duplicates in multiple groups
4985 4990 continue
4986 4991
4987 4992 users[key] = {
4988 4993 'user': member.user,
4989 4994 'source': 'user_group',
4990 4995 'source_data': source_data,
4991 4996 'data': rule_user_group.rule_data()
4992 4997 }
4993 4998
4994 4999 return users
4995 5000
4996 5001 def user_group_vote_rule(self, user_id):
4997 5002
4998 5003 rules = []
4999 5004 if not self.rule_user_groups:
5000 5005 return rules
5001 5006
5002 5007 for user_group in self.rule_user_groups:
5003 5008 user_group_members = [x.user_id for x in user_group.users_group.members]
5004 5009 if user_id in user_group_members:
5005 5010 rules.append(user_group)
5006 5011 return rules
5007 5012
5008 5013 def __repr__(self):
5009 5014 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5010 5015 self.repo_review_rule_id, self.repo)
5011 5016
5012 5017
5013 5018 class ScheduleEntry(Base, BaseModel):
5014 5019 __tablename__ = 'schedule_entries'
5015 5020 __table_args__ = (
5016 5021 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5017 5022 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5018 5023 base_table_args,
5019 5024 )
5020 5025
5021 5026 schedule_types = ['crontab', 'timedelta', 'integer']
5022 5027 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5023 5028
5024 5029 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5025 5030 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5026 5031 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5027 5032
5028 5033 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5029 5034 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5030 5035
5031 5036 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5032 5037 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5033 5038
5034 5039 # task
5035 5040 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5036 5041 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5037 5042 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5038 5043 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5039 5044
5040 5045 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5041 5046 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5042 5047
5043 5048 @hybrid_property
5044 5049 def schedule_type(self):
5045 5050 return self._schedule_type
5046 5051
5047 5052 @schedule_type.setter
5048 5053 def schedule_type(self, val):
5049 5054 if val not in self.schedule_types:
5050 5055 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5051 5056 val, self.schedule_type))
5052 5057
5053 5058 self._schedule_type = val
5054 5059
5055 5060 @classmethod
5056 5061 def get_uid(cls, obj):
5057 5062 args = obj.task_args
5058 5063 kwargs = obj.task_kwargs
5059 5064 if isinstance(args, JsonRaw):
5060 5065 try:
5061 5066 args = json.loads(args)
5062 5067 except ValueError:
5063 5068 args = tuple()
5064 5069
5065 5070 if isinstance(kwargs, JsonRaw):
5066 5071 try:
5067 5072 kwargs = json.loads(kwargs)
5068 5073 except ValueError:
5069 5074 kwargs = dict()
5070 5075
5071 5076 dot_notation = obj.task_dot_notation
5072 5077 val = '.'.join(map(safe_str, [
5073 5078 sorted(dot_notation), args, sorted(kwargs.items())]))
5074 5079 return hashlib.sha1(val).hexdigest()
5075 5080
5076 5081 @classmethod
5077 5082 def get_by_schedule_name(cls, schedule_name):
5078 5083 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5079 5084
5080 5085 @classmethod
5081 5086 def get_by_schedule_id(cls, schedule_id):
5082 5087 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5083 5088
5084 5089 @property
5085 5090 def task(self):
5086 5091 return self.task_dot_notation
5087 5092
5088 5093 @property
5089 5094 def schedule(self):
5090 5095 from rhodecode.lib.celerylib.utils import raw_2_schedule
5091 5096 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5092 5097 return schedule
5093 5098
5094 5099 @property
5095 5100 def args(self):
5096 5101 try:
5097 5102 return list(self.task_args or [])
5098 5103 except ValueError:
5099 5104 return list()
5100 5105
5101 5106 @property
5102 5107 def kwargs(self):
5103 5108 try:
5104 5109 return dict(self.task_kwargs or {})
5105 5110 except ValueError:
5106 5111 return dict()
5107 5112
5108 5113 def _as_raw(self, val):
5109 5114 if hasattr(val, 'de_coerce'):
5110 5115 val = val.de_coerce()
5111 5116 if val:
5112 5117 val = json.dumps(val)
5113 5118
5114 5119 return val
5115 5120
5116 5121 @property
5117 5122 def schedule_definition_raw(self):
5118 5123 return self._as_raw(self.schedule_definition)
5119 5124
5120 5125 @property
5121 5126 def args_raw(self):
5122 5127 return self._as_raw(self.task_args)
5123 5128
5124 5129 @property
5125 5130 def kwargs_raw(self):
5126 5131 return self._as_raw(self.task_kwargs)
5127 5132
5128 5133 def __repr__(self):
5129 5134 return '<DB:ScheduleEntry({}:{})>'.format(
5130 5135 self.schedule_entry_id, self.schedule_name)
5131 5136
5132 5137
5133 5138 @event.listens_for(ScheduleEntry, 'before_update')
5134 5139 def update_task_uid(mapper, connection, target):
5135 5140 target.task_uid = ScheduleEntry.get_uid(target)
5136 5141
5137 5142
5138 5143 @event.listens_for(ScheduleEntry, 'before_insert')
5139 5144 def set_task_uid(mapper, connection, target):
5140 5145 target.task_uid = ScheduleEntry.get_uid(target)
5141 5146
5142 5147
5143 5148 class _BaseBranchPerms(BaseModel):
5144 5149 @classmethod
5145 5150 def compute_hash(cls, value):
5146 5151 return sha1_safe(value)
5147 5152
5148 5153 @hybrid_property
5149 5154 def branch_pattern(self):
5150 5155 return self._branch_pattern or '*'
5151 5156
5152 5157 @hybrid_property
5153 5158 def branch_hash(self):
5154 5159 return self._branch_hash
5155 5160
5156 5161 def _validate_glob(self, value):
5157 5162 re.compile('^' + glob2re(value) + '$')
5158 5163
5159 5164 @branch_pattern.setter
5160 5165 def branch_pattern(self, value):
5161 5166 self._validate_glob(value)
5162 5167 self._branch_pattern = value or '*'
5163 5168 # set the Hash when setting the branch pattern
5164 5169 self._branch_hash = self.compute_hash(self._branch_pattern)
5165 5170
5166 5171 def matches(self, branch):
5167 5172 """
5168 5173 Check if this the branch matches entry
5169 5174
5170 5175 :param branch: branch name for the commit
5171 5176 """
5172 5177
5173 5178 branch = branch or ''
5174 5179
5175 5180 branch_matches = True
5176 5181 if branch:
5177 5182 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5178 5183 branch_matches = bool(branch_regex.search(branch))
5179 5184
5180 5185 return branch_matches
5181 5186
5182 5187
5183 5188 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5184 5189 __tablename__ = 'user_to_repo_branch_permissions'
5185 5190 __table_args__ = (
5186 5191 base_table_args
5187 5192 )
5188 5193
5189 5194 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5190 5195
5191 5196 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5192 5197 repo = relationship('Repository', backref='user_branch_perms')
5193 5198
5194 5199 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5195 5200 permission = relationship('Permission')
5196 5201
5197 5202 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5198 5203 user_repo_to_perm = relationship('UserRepoToPerm')
5199 5204
5200 5205 rule_order = Column('rule_order', Integer(), nullable=False)
5201 5206 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5202 5207 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5203 5208
5204 5209 def __unicode__(self):
5205 5210 return u'<UserBranchPermission(%s => %r)>' % (
5206 5211 self.user_repo_to_perm, self.branch_pattern)
5207 5212
5208 5213
5209 5214 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5210 5215 __tablename__ = 'user_group_to_repo_branch_permissions'
5211 5216 __table_args__ = (
5212 5217 base_table_args
5213 5218 )
5214 5219
5215 5220 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5216 5221
5217 5222 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5218 5223 repo = relationship('Repository', backref='user_group_branch_perms')
5219 5224
5220 5225 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5221 5226 permission = relationship('Permission')
5222 5227
5223 5228 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5224 5229 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5225 5230
5226 5231 rule_order = Column('rule_order', Integer(), nullable=False)
5227 5232 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5228 5233 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5229 5234
5230 5235 def __unicode__(self):
5231 5236 return u'<UserBranchPermission(%s => %r)>' % (
5232 5237 self.user_group_repo_to_perm, self.branch_pattern)
5233 5238
5234 5239
5235 5240 class UserBookmark(Base, BaseModel):
5236 5241 __tablename__ = 'user_bookmarks'
5237 5242 __table_args__ = (
5238 5243 UniqueConstraint('user_id', 'bookmark_repo_id'),
5239 5244 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5240 5245 UniqueConstraint('user_id', 'bookmark_position'),
5241 5246 base_table_args
5242 5247 )
5243 5248
5244 5249 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5245 5250 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5246 5251 position = Column("bookmark_position", Integer(), nullable=False)
5247 5252 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5248 5253 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5249 5254 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5250 5255
5251 5256 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5252 5257 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5253 5258
5254 5259 user = relationship("User")
5255 5260
5256 5261 repository = relationship("Repository")
5257 5262 repository_group = relationship("RepoGroup")
5258 5263
5259 5264 @classmethod
5260 5265 def get_by_position_for_user(cls, position, user_id):
5261 5266 return cls.query() \
5262 5267 .filter(UserBookmark.user_id == user_id) \
5263 5268 .filter(UserBookmark.position == position).scalar()
5264 5269
5265 5270 @classmethod
5266 5271 def get_bookmarks_for_user(cls, user_id, cache=True):
5267 5272 bookmarks = cls.query() \
5268 5273 .filter(UserBookmark.user_id == user_id) \
5269 5274 .options(joinedload(UserBookmark.repository)) \
5270 5275 .options(joinedload(UserBookmark.repository_group)) \
5271 5276 .order_by(UserBookmark.position.asc())
5272 5277
5273 5278 if cache:
5274 5279 bookmarks = bookmarks.options(
5275 5280 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5276 5281 )
5277 5282
5278 5283 return bookmarks.all()
5279 5284
5280 5285 def __unicode__(self):
5281 5286 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5282 5287
5283 5288
5284 5289 class FileStore(Base, BaseModel):
5285 5290 __tablename__ = 'file_store'
5286 5291 __table_args__ = (
5287 5292 base_table_args
5288 5293 )
5289 5294
5290 5295 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5291 5296 file_uid = Column('file_uid', String(1024), nullable=False)
5292 5297 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5293 5298 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5294 5299 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5295 5300
5296 5301 # sha256 hash
5297 5302 file_hash = Column('file_hash', String(512), nullable=False)
5298 5303 file_size = Column('file_size', BigInteger(), nullable=False)
5299 5304
5300 5305 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5301 5306 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5302 5307 accessed_count = Column('accessed_count', Integer(), default=0)
5303 5308
5304 5309 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5305 5310
5306 5311 # if repo/repo_group reference is set, check for permissions
5307 5312 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5308 5313
5309 5314 # hidden defines an attachment that should be hidden from showing in artifact listing
5310 5315 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5311 5316
5312 5317 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5313 5318 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5314 5319
5315 5320 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5316 5321
5317 5322 # scope limited to user, which requester have access to
5318 5323 scope_user_id = Column(
5319 5324 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5320 5325 nullable=True, unique=None, default=None)
5321 5326 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5322 5327
5323 5328 # scope limited to user group, which requester have access to
5324 5329 scope_user_group_id = Column(
5325 5330 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5326 5331 nullable=True, unique=None, default=None)
5327 5332 user_group = relationship('UserGroup', lazy='joined')
5328 5333
5329 5334 # scope limited to repo, which requester have access to
5330 5335 scope_repo_id = Column(
5331 5336 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5332 5337 nullable=True, unique=None, default=None)
5333 5338 repo = relationship('Repository', lazy='joined')
5334 5339
5335 5340 # scope limited to repo group, which requester have access to
5336 5341 scope_repo_group_id = Column(
5337 5342 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5338 5343 nullable=True, unique=None, default=None)
5339 5344 repo_group = relationship('RepoGroup', lazy='joined')
5340 5345
5341 5346 @classmethod
5342 5347 def get_by_store_uid(cls, file_store_uid):
5343 5348 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5344 5349
5345 5350 @classmethod
5346 5351 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5347 5352 file_description='', enabled=True, hidden=False, check_acl=True,
5348 5353 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5349 5354
5350 5355 store_entry = FileStore()
5351 5356 store_entry.file_uid = file_uid
5352 5357 store_entry.file_display_name = file_display_name
5353 5358 store_entry.file_org_name = filename
5354 5359 store_entry.file_size = file_size
5355 5360 store_entry.file_hash = file_hash
5356 5361 store_entry.file_description = file_description
5357 5362
5358 5363 store_entry.check_acl = check_acl
5359 5364 store_entry.enabled = enabled
5360 5365 store_entry.hidden = hidden
5361 5366
5362 5367 store_entry.user_id = user_id
5363 5368 store_entry.scope_user_id = scope_user_id
5364 5369 store_entry.scope_repo_id = scope_repo_id
5365 5370 store_entry.scope_repo_group_id = scope_repo_group_id
5366 5371
5367 5372 return store_entry
5368 5373
5369 5374 @classmethod
5370 5375 def store_metadata(cls, file_store_id, args, commit=True):
5371 5376 file_store = FileStore.get(file_store_id)
5372 5377 if file_store is None:
5373 5378 return
5374 5379
5375 5380 for section, key, value, value_type in args:
5376 5381 has_key = FileStoreMetadata().query() \
5377 5382 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5378 5383 .filter(FileStoreMetadata.file_store_meta_section == section) \
5379 5384 .filter(FileStoreMetadata.file_store_meta_key == key) \
5380 5385 .scalar()
5381 5386 if has_key:
5382 5387 msg = 'key `{}` already defined under section `{}` for this file.'\
5383 5388 .format(key, section)
5384 5389 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5385 5390
5386 5391 # NOTE(marcink): raises ArtifactMetadataBadValueType
5387 5392 FileStoreMetadata.valid_value_type(value_type)
5388 5393
5389 5394 meta_entry = FileStoreMetadata()
5390 5395 meta_entry.file_store = file_store
5391 5396 meta_entry.file_store_meta_section = section
5392 5397 meta_entry.file_store_meta_key = key
5393 5398 meta_entry.file_store_meta_value_type = value_type
5394 5399 meta_entry.file_store_meta_value = value
5395 5400
5396 5401 Session().add(meta_entry)
5397 5402
5398 5403 try:
5399 5404 if commit:
5400 5405 Session().commit()
5401 5406 except IntegrityError:
5402 5407 Session().rollback()
5403 5408 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5404 5409
5405 5410 @classmethod
5406 5411 def bump_access_counter(cls, file_uid, commit=True):
5407 5412 FileStore().query()\
5408 5413 .filter(FileStore.file_uid == file_uid)\
5409 5414 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5410 5415 FileStore.accessed_on: datetime.datetime.now()})
5411 5416 if commit:
5412 5417 Session().commit()
5413 5418
5414 5419 def __json__(self):
5415 5420 data = {
5416 5421 'filename': self.file_display_name,
5417 5422 'filename_org': self.file_org_name,
5418 5423 'file_uid': self.file_uid,
5419 5424 'description': self.file_description,
5420 5425 'hidden': self.hidden,
5421 5426 'size': self.file_size,
5422 5427 'created_on': self.created_on,
5423 5428 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5424 5429 'downloaded_times': self.accessed_count,
5425 5430 'sha256': self.file_hash,
5426 5431 'metadata': self.file_metadata,
5427 5432 }
5428 5433
5429 5434 return data
5430 5435
5431 5436 def __repr__(self):
5432 5437 return '<FileStore({})>'.format(self.file_store_id)
5433 5438
5434 5439
5435 5440 class FileStoreMetadata(Base, BaseModel):
5436 5441 __tablename__ = 'file_store_metadata'
5437 5442 __table_args__ = (
5438 5443 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5439 5444 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5440 5445 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5441 5446 base_table_args
5442 5447 )
5443 5448 SETTINGS_TYPES = {
5444 5449 'str': safe_str,
5445 5450 'int': safe_int,
5446 5451 'unicode': safe_unicode,
5447 5452 'bool': str2bool,
5448 5453 'list': functools.partial(aslist, sep=',')
5449 5454 }
5450 5455
5451 5456 file_store_meta_id = Column(
5452 5457 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5453 5458 primary_key=True)
5454 5459 _file_store_meta_section = Column(
5455 5460 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5456 5461 nullable=True, unique=None, default=None)
5457 5462 _file_store_meta_section_hash = Column(
5458 5463 "file_store_meta_section_hash", String(255),
5459 5464 nullable=True, unique=None, default=None)
5460 5465 _file_store_meta_key = Column(
5461 5466 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5462 5467 nullable=True, unique=None, default=None)
5463 5468 _file_store_meta_key_hash = Column(
5464 5469 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5465 5470 _file_store_meta_value = Column(
5466 5471 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5467 5472 nullable=True, unique=None, default=None)
5468 5473 _file_store_meta_value_type = Column(
5469 5474 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5470 5475 default='unicode')
5471 5476
5472 5477 file_store_id = Column(
5473 5478 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5474 5479 nullable=True, unique=None, default=None)
5475 5480
5476 5481 file_store = relationship('FileStore', lazy='joined')
5477 5482
5478 5483 @classmethod
5479 5484 def valid_value_type(cls, value):
5480 5485 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5481 5486 raise ArtifactMetadataBadValueType(
5482 5487 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5483 5488
5484 5489 @hybrid_property
5485 5490 def file_store_meta_section(self):
5486 5491 return self._file_store_meta_section
5487 5492
5488 5493 @file_store_meta_section.setter
5489 5494 def file_store_meta_section(self, value):
5490 5495 self._file_store_meta_section = value
5491 5496 self._file_store_meta_section_hash = _hash_key(value)
5492 5497
5493 5498 @hybrid_property
5494 5499 def file_store_meta_key(self):
5495 5500 return self._file_store_meta_key
5496 5501
5497 5502 @file_store_meta_key.setter
5498 5503 def file_store_meta_key(self, value):
5499 5504 self._file_store_meta_key = value
5500 5505 self._file_store_meta_key_hash = _hash_key(value)
5501 5506
5502 5507 @hybrid_property
5503 5508 def file_store_meta_value(self):
5504 5509 val = self._file_store_meta_value
5505 5510
5506 5511 if self._file_store_meta_value_type:
5507 5512 # e.g unicode.encrypted == unicode
5508 5513 _type = self._file_store_meta_value_type.split('.')[0]
5509 5514 # decode the encrypted value if it's encrypted field type
5510 5515 if '.encrypted' in self._file_store_meta_value_type:
5511 5516 cipher = EncryptedTextValue()
5512 5517 val = safe_unicode(cipher.process_result_value(val, None))
5513 5518 # do final type conversion
5514 5519 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5515 5520 val = converter(val)
5516 5521
5517 5522 return val
5518 5523
5519 5524 @file_store_meta_value.setter
5520 5525 def file_store_meta_value(self, val):
5521 5526 val = safe_unicode(val)
5522 5527 # encode the encrypted value
5523 5528 if '.encrypted' in self.file_store_meta_value_type:
5524 5529 cipher = EncryptedTextValue()
5525 5530 val = safe_unicode(cipher.process_bind_param(val, None))
5526 5531 self._file_store_meta_value = val
5527 5532
5528 5533 @hybrid_property
5529 5534 def file_store_meta_value_type(self):
5530 5535 return self._file_store_meta_value_type
5531 5536
5532 5537 @file_store_meta_value_type.setter
5533 5538 def file_store_meta_value_type(self, val):
5534 5539 # e.g unicode.encrypted
5535 5540 self.valid_value_type(val)
5536 5541 self._file_store_meta_value_type = val
5537 5542
5538 5543 def __json__(self):
5539 5544 data = {
5540 5545 'artifact': self.file_store.file_uid,
5541 5546 'section': self.file_store_meta_section,
5542 5547 'key': self.file_store_meta_key,
5543 5548 'value': self.file_store_meta_value,
5544 5549 }
5545 5550
5546 5551 return data
5547 5552
5548 5553 def __repr__(self):
5549 5554 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5550 5555 self.file_store_meta_key, self.file_store_meta_value)
5551 5556
5552 5557
5553 5558 class DbMigrateVersion(Base, BaseModel):
5554 5559 __tablename__ = 'db_migrate_version'
5555 5560 __table_args__ = (
5556 5561 base_table_args,
5557 5562 )
5558 5563
5559 5564 repository_id = Column('repository_id', String(250), primary_key=True)
5560 5565 repository_path = Column('repository_path', Text)
5561 5566 version = Column('version', Integer)
5562 5567
5563 5568 @classmethod
5564 5569 def set_version(cls, version):
5565 5570 """
5566 5571 Helper for forcing a different version, usually for debugging purposes via ishell.
5567 5572 """
5568 5573 ver = DbMigrateVersion.query().first()
5569 5574 ver.version = version
5570 5575 Session().commit()
5571 5576
5572 5577
5573 5578 class DbSession(Base, BaseModel):
5574 5579 __tablename__ = 'db_session'
5575 5580 __table_args__ = (
5576 5581 base_table_args,
5577 5582 )
5578 5583
5579 5584 def __repr__(self):
5580 5585 return '<DB:DbSession({})>'.format(self.id)
5581 5586
5582 5587 id = Column('id', Integer())
5583 5588 namespace = Column('namespace', String(255), primary_key=True)
5584 5589 accessed = Column('accessed', DateTime, nullable=False)
5585 5590 created = Column('created', DateTime, nullable=False)
5586 5591 data = Column('data', PickleType, nullable=False)
@@ -1,597 +1,597 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 permissions model for RhodeCode
23 23 """
24 24 import collections
25 25 import logging
26 26 import traceback
27 27
28 28 from sqlalchemy.exc import DatabaseError
29 29
30 30 from rhodecode import events
31 31 from rhodecode.model import BaseModel
32 32 from rhodecode.model.db import (
33 33 User, Permission, UserToPerm, UserRepoToPerm, UserRepoGroupToPerm,
34 34 UserUserGroupToPerm, UserGroup, UserGroupToPerm, UserToRepoBranchPermission)
35 35 from rhodecode.lib.utils2 import str2bool, safe_int
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class PermissionModel(BaseModel):
41 41 """
42 42 Permissions model for RhodeCode
43 43 """
44 44
45 45 cls = Permission
46 46 global_perms = {
47 47 'default_repo_create': None,
48 48 # special case for create repos on write access to group
49 49 'default_repo_create_on_write': None,
50 50 'default_repo_group_create': None,
51 51 'default_user_group_create': None,
52 52 'default_fork_create': None,
53 53 'default_inherit_default_permissions': None,
54 54 'default_register': None,
55 55 'default_password_reset': None,
56 56 'default_extern_activate': None,
57 57
58 58 # object permissions below
59 59 'default_repo_perm': None,
60 60 'default_group_perm': None,
61 61 'default_user_group_perm': None,
62 62
63 63 # branch
64 64 'default_branch_perm': None,
65 65 }
66 66
67 67 def set_global_permission_choices(self, c_obj, gettext_translator):
68 68 _ = gettext_translator
69 69
70 70 c_obj.repo_perms_choices = [
71 71 ('repository.none', _('None'),),
72 72 ('repository.read', _('Read'),),
73 73 ('repository.write', _('Write'),),
74 74 ('repository.admin', _('Admin'),)]
75 75
76 76 c_obj.group_perms_choices = [
77 77 ('group.none', _('None'),),
78 78 ('group.read', _('Read'),),
79 79 ('group.write', _('Write'),),
80 80 ('group.admin', _('Admin'),)]
81 81
82 82 c_obj.user_group_perms_choices = [
83 83 ('usergroup.none', _('None'),),
84 84 ('usergroup.read', _('Read'),),
85 85 ('usergroup.write', _('Write'),),
86 86 ('usergroup.admin', _('Admin'),)]
87 87
88 88 c_obj.branch_perms_choices = [
89 89 ('branch.none', _('Protected/No Access'),),
90 90 ('branch.merge', _('Web merge'),),
91 91 ('branch.push', _('Push'),),
92 92 ('branch.push_force', _('Force Push'),)]
93 93
94 94 c_obj.register_choices = [
95 95 ('hg.register.none', _('Disabled')),
96 96 ('hg.register.manual_activate', _('Allowed with manual account activation')),
97 97 ('hg.register.auto_activate', _('Allowed with automatic account activation')),]
98 98
99 99 c_obj.password_reset_choices = [
100 100 ('hg.password_reset.enabled', _('Allow password recovery')),
101 101 ('hg.password_reset.hidden', _('Hide password recovery link')),
102 102 ('hg.password_reset.disabled', _('Disable password recovery')),]
103 103
104 104 c_obj.extern_activate_choices = [
105 105 ('hg.extern_activate.manual', _('Manual activation of external account')),
106 106 ('hg.extern_activate.auto', _('Automatic activation of external account')),]
107 107
108 108 c_obj.repo_create_choices = [
109 109 ('hg.create.none', _('Disabled')),
110 110 ('hg.create.repository', _('Enabled'))]
111 111
112 112 c_obj.repo_create_on_write_choices = [
113 113 ('hg.create.write_on_repogroup.false', _('Disabled')),
114 114 ('hg.create.write_on_repogroup.true', _('Enabled'))]
115 115
116 116 c_obj.user_group_create_choices = [
117 117 ('hg.usergroup.create.false', _('Disabled')),
118 118 ('hg.usergroup.create.true', _('Enabled'))]
119 119
120 120 c_obj.repo_group_create_choices = [
121 121 ('hg.repogroup.create.false', _('Disabled')),
122 122 ('hg.repogroup.create.true', _('Enabled'))]
123 123
124 124 c_obj.fork_choices = [
125 125 ('hg.fork.none', _('Disabled')),
126 126 ('hg.fork.repository', _('Enabled'))]
127 127
128 128 c_obj.inherit_default_permission_choices = [
129 129 ('hg.inherit_default_perms.false', _('Disabled')),
130 130 ('hg.inherit_default_perms.true', _('Enabled'))]
131 131
132 132 def get_default_perms(self, object_perms, suffix):
133 133 defaults = {}
134 134 for perm in object_perms:
135 135 # perms
136 136 if perm.permission.permission_name.startswith('repository.'):
137 137 defaults['default_repo_perm' + suffix] = perm.permission.permission_name
138 138
139 139 if perm.permission.permission_name.startswith('group.'):
140 140 defaults['default_group_perm' + suffix] = perm.permission.permission_name
141 141
142 142 if perm.permission.permission_name.startswith('usergroup.'):
143 143 defaults['default_user_group_perm' + suffix] = perm.permission.permission_name
144 144
145 145 # branch
146 146 if perm.permission.permission_name.startswith('branch.'):
147 147 defaults['default_branch_perm' + suffix] = perm.permission.permission_name
148 148
149 149 # creation of objects
150 150 if perm.permission.permission_name.startswith('hg.create.write_on_repogroup'):
151 151 defaults['default_repo_create_on_write' + suffix] = perm.permission.permission_name
152 152
153 153 elif perm.permission.permission_name.startswith('hg.create.'):
154 154 defaults['default_repo_create' + suffix] = perm.permission.permission_name
155 155
156 156 if perm.permission.permission_name.startswith('hg.fork.'):
157 157 defaults['default_fork_create' + suffix] = perm.permission.permission_name
158 158
159 159 if perm.permission.permission_name.startswith('hg.inherit_default_perms.'):
160 160 defaults['default_inherit_default_permissions' + suffix] = perm.permission.permission_name
161 161
162 162 if perm.permission.permission_name.startswith('hg.repogroup.'):
163 163 defaults['default_repo_group_create' + suffix] = perm.permission.permission_name
164 164
165 165 if perm.permission.permission_name.startswith('hg.usergroup.'):
166 166 defaults['default_user_group_create' + suffix] = perm.permission.permission_name
167 167
168 168 # registration and external account activation
169 169 if perm.permission.permission_name.startswith('hg.register.'):
170 170 defaults['default_register' + suffix] = perm.permission.permission_name
171 171
172 172 if perm.permission.permission_name.startswith('hg.password_reset.'):
173 173 defaults['default_password_reset' + suffix] = perm.permission.permission_name
174 174
175 175 if perm.permission.permission_name.startswith('hg.extern_activate.'):
176 176 defaults['default_extern_activate' + suffix] = perm.permission.permission_name
177 177
178 178 return defaults
179 179
180 180 def _make_new_user_perm(self, user, perm_name):
181 181 log.debug('Creating new user permission:%s', perm_name)
182 182 new = UserToPerm()
183 183 new.user = user
184 184 new.permission = Permission.get_by_key(perm_name)
185 185 return new
186 186
187 187 def _make_new_user_group_perm(self, user_group, perm_name):
188 188 log.debug('Creating new user group permission:%s', perm_name)
189 189 new = UserGroupToPerm()
190 190 new.users_group = user_group
191 191 new.permission = Permission.get_by_key(perm_name)
192 192 return new
193 193
194 194 def _keep_perm(self, perm_name, keep_fields):
195 195 def get_pat(field_name):
196 196 return {
197 197 # global perms
198 198 'default_repo_create': 'hg.create.',
199 199 # special case for create repos on write access to group
200 200 'default_repo_create_on_write': 'hg.create.write_on_repogroup.',
201 201 'default_repo_group_create': 'hg.repogroup.create.',
202 202 'default_user_group_create': 'hg.usergroup.create.',
203 203 'default_fork_create': 'hg.fork.',
204 204 'default_inherit_default_permissions': 'hg.inherit_default_perms.',
205 205
206 206 # application perms
207 207 'default_register': 'hg.register.',
208 208 'default_password_reset': 'hg.password_reset.',
209 209 'default_extern_activate': 'hg.extern_activate.',
210 210
211 211 # object permissions below
212 212 'default_repo_perm': 'repository.',
213 213 'default_group_perm': 'group.',
214 214 'default_user_group_perm': 'usergroup.',
215 215 # branch
216 216 'default_branch_perm': 'branch.',
217 217
218 218 }[field_name]
219 219 for field in keep_fields:
220 220 pat = get_pat(field)
221 221 if perm_name.startswith(pat):
222 222 return True
223 223 return False
224 224
225 225 def _clear_object_perm(self, object_perms, preserve=None):
226 226 preserve = preserve or []
227 227 _deleted = []
228 228 for perm in object_perms:
229 229 perm_name = perm.permission.permission_name
230 230 if not self._keep_perm(perm_name, keep_fields=preserve):
231 231 _deleted.append(perm_name)
232 232 self.sa.delete(perm)
233 233 return _deleted
234 234
235 235 def _clear_user_perms(self, user_id, preserve=None):
236 236 perms = self.sa.query(UserToPerm)\
237 237 .filter(UserToPerm.user_id == user_id)\
238 238 .all()
239 239 return self._clear_object_perm(perms, preserve=preserve)
240 240
241 241 def _clear_user_group_perms(self, user_group_id, preserve=None):
242 242 perms = self.sa.query(UserGroupToPerm)\
243 243 .filter(UserGroupToPerm.users_group_id == user_group_id)\
244 244 .all()
245 245 return self._clear_object_perm(perms, preserve=preserve)
246 246
247 247 def _set_new_object_perms(self, obj_type, object, form_result, preserve=None):
248 248 # clear current entries, to make this function idempotent
249 249 # it will fix even if we define more permissions or permissions
250 250 # are somehow missing
251 251 preserve = preserve or []
252 252 _global_perms = self.global_perms.copy()
253 253 if obj_type not in ['user', 'user_group']:
254 254 raise ValueError("obj_type must be on of 'user' or 'user_group'")
255 255 global_perms = len(_global_perms)
256 256 default_user_perms = len(Permission.DEFAULT_USER_PERMISSIONS)
257 257 if global_perms != default_user_perms:
258 258 raise Exception(
259 259 'Inconsistent permissions definition. Got {} vs {}'.format(
260 260 global_perms, default_user_perms))
261 261
262 262 if obj_type == 'user':
263 263 self._clear_user_perms(object.user_id, preserve)
264 264 if obj_type == 'user_group':
265 265 self._clear_user_group_perms(object.users_group_id, preserve)
266 266
267 267 # now kill the keys that we want to preserve from the form.
268 268 for key in preserve:
269 269 del _global_perms[key]
270 270
271 271 for k in _global_perms.copy():
272 272 _global_perms[k] = form_result[k]
273 273
274 274 # at that stage we validate all are passed inside form_result
275 275 for _perm_key, perm_value in _global_perms.items():
276 276 if perm_value is None:
277 277 raise ValueError('Missing permission for %s' % (_perm_key,))
278 278
279 279 if obj_type == 'user':
280 280 p = self._make_new_user_perm(object, perm_value)
281 281 self.sa.add(p)
282 282 if obj_type == 'user_group':
283 283 p = self._make_new_user_group_perm(object, perm_value)
284 284 self.sa.add(p)
285 285
286 286 def _set_new_user_perms(self, user, form_result, preserve=None):
287 287 return self._set_new_object_perms(
288 288 'user', user, form_result, preserve)
289 289
290 290 def _set_new_user_group_perms(self, user_group, form_result, preserve=None):
291 291 return self._set_new_object_perms(
292 292 'user_group', user_group, form_result, preserve)
293 293
294 294 def set_new_user_perms(self, user, form_result):
295 295 # calculate what to preserve from what is given in form_result
296 296 preserve = set(self.global_perms.keys()).difference(set(form_result.keys()))
297 297 return self._set_new_user_perms(user, form_result, preserve)
298 298
299 299 def set_new_user_group_perms(self, user_group, form_result):
300 300 # calculate what to preserve from what is given in form_result
301 301 preserve = set(self.global_perms.keys()).difference(set(form_result.keys()))
302 302 return self._set_new_user_group_perms(user_group, form_result, preserve)
303 303
304 304 def create_permissions(self):
305 305 """
306 306 Create permissions for whole system
307 307 """
308 308 for p in Permission.PERMS:
309 309 if not Permission.get_by_key(p[0]):
310 310 new_perm = Permission()
311 311 new_perm.permission_name = p[0]
312 312 new_perm.permission_longname = p[0] # translation err with p[1]
313 313 self.sa.add(new_perm)
314 314
315 315 def _create_default_object_permission(self, obj_type, obj, obj_perms,
316 316 force=False):
317 317 if obj_type not in ['user', 'user_group']:
318 318 raise ValueError("obj_type must be on of 'user' or 'user_group'")
319 319
320 320 def _get_group(perm_name):
321 321 return '.'.join(perm_name.split('.')[:1])
322 322
323 323 defined_perms_groups = map(
324 324 _get_group, (x.permission.permission_name for x in obj_perms))
325 325 log.debug('GOT ALREADY DEFINED:%s', obj_perms)
326 326
327 327 if force:
328 328 self._clear_object_perm(obj_perms)
329 329 self.sa.commit()
330 330 defined_perms_groups = []
331 331 # for every default permission that needs to be created, we check if
332 332 # it's group is already defined, if it's not we create default perm
333 333 for perm_name in Permission.DEFAULT_USER_PERMISSIONS:
334 334 gr = _get_group(perm_name)
335 335 if gr not in defined_perms_groups:
336 336 log.debug('GR:%s not found, creating permission %s',
337 337 gr, perm_name)
338 338 if obj_type == 'user':
339 339 new_perm = self._make_new_user_perm(obj, perm_name)
340 340 self.sa.add(new_perm)
341 341 if obj_type == 'user_group':
342 342 new_perm = self._make_new_user_group_perm(obj, perm_name)
343 343 self.sa.add(new_perm)
344 344
345 345 def create_default_user_permissions(self, user, force=False):
346 346 """
347 347 Creates only missing default permissions for user, if force is set it
348 348 resets the default permissions for that user
349 349
350 350 :param user:
351 351 :param force:
352 352 """
353 353 user = self._get_user(user)
354 354 obj_perms = UserToPerm.query().filter(UserToPerm.user == user).all()
355 355 return self._create_default_object_permission(
356 356 'user', user, obj_perms, force)
357 357
358 358 def create_default_user_group_permissions(self, user_group, force=False):
359 359 """
360 360 Creates only missing default permissions for user group, if force is
361 361 set it resets the default permissions for that user group
362 362
363 363 :param user_group:
364 364 :param force:
365 365 """
366 366 user_group = self._get_user_group(user_group)
367 367 obj_perms = UserToPerm.query().filter(UserGroupToPerm.users_group == user_group).all()
368 368 return self._create_default_object_permission(
369 369 'user_group', user_group, obj_perms, force)
370 370
371 371 def update_application_permissions(self, form_result):
372 372 if 'perm_user_id' in form_result:
373 373 perm_user = User.get(safe_int(form_result['perm_user_id']))
374 374 else:
375 375 # used mostly to do lookup for default user
376 376 perm_user = User.get_by_username(form_result['perm_user_name'])
377 377
378 378 try:
379 379 # stage 1 set anonymous access
380 380 if perm_user.username == User.DEFAULT_USER:
381 381 perm_user.active = str2bool(form_result['anonymous'])
382 382 self.sa.add(perm_user)
383 383
384 384 # stage 2 reset defaults and set them from form data
385 385 self._set_new_user_perms(perm_user, form_result, preserve=[
386 386 'default_repo_perm',
387 387 'default_group_perm',
388 388 'default_user_group_perm',
389 389 'default_branch_perm',
390 390
391 391 'default_repo_group_create',
392 392 'default_user_group_create',
393 393 'default_repo_create_on_write',
394 394 'default_repo_create',
395 395 'default_fork_create',
396 396 'default_inherit_default_permissions',])
397 397
398 398 self.sa.commit()
399 399 except (DatabaseError,):
400 400 log.error(traceback.format_exc())
401 401 self.sa.rollback()
402 402 raise
403 403
404 404 def update_user_permissions(self, form_result):
405 405 if 'perm_user_id' in form_result:
406 406 perm_user = User.get(safe_int(form_result['perm_user_id']))
407 407 else:
408 408 # used mostly to do lookup for default user
409 409 perm_user = User.get_by_username(form_result['perm_user_name'])
410 410 try:
411 411 # stage 2 reset defaults and set them from form data
412 412 self._set_new_user_perms(perm_user, form_result, preserve=[
413 413 'default_repo_perm',
414 414 'default_group_perm',
415 415 'default_user_group_perm',
416 416 'default_branch_perm',
417 417
418 418 'default_register',
419 419 'default_password_reset',
420 420 'default_extern_activate'])
421 421 self.sa.commit()
422 422 except (DatabaseError,):
423 423 log.error(traceback.format_exc())
424 424 self.sa.rollback()
425 425 raise
426 426
427 427 def update_user_group_permissions(self, form_result):
428 428 if 'perm_user_group_id' in form_result:
429 429 perm_user_group = UserGroup.get(safe_int(form_result['perm_user_group_id']))
430 430 else:
431 431 # used mostly to do lookup for default user
432 432 perm_user_group = UserGroup.get_by_group_name(form_result['perm_user_group_name'])
433 433 try:
434 434 # stage 2 reset defaults and set them from form data
435 435 self._set_new_user_group_perms(perm_user_group, form_result, preserve=[
436 436 'default_repo_perm',
437 437 'default_group_perm',
438 438 'default_user_group_perm',
439 439 'default_branch_perm',
440 440
441 441 'default_register',
442 442 'default_password_reset',
443 443 'default_extern_activate'])
444 444 self.sa.commit()
445 445 except (DatabaseError,):
446 446 log.error(traceback.format_exc())
447 447 self.sa.rollback()
448 448 raise
449 449
450 450 def update_object_permissions(self, form_result):
451 451 if 'perm_user_id' in form_result:
452 452 perm_user = User.get(safe_int(form_result['perm_user_id']))
453 453 else:
454 454 # used mostly to do lookup for default user
455 455 perm_user = User.get_by_username(form_result['perm_user_name'])
456 456 try:
457 457
458 458 # stage 2 reset defaults and set them from form data
459 459 self._set_new_user_perms(perm_user, form_result, preserve=[
460 460 'default_repo_group_create',
461 461 'default_user_group_create',
462 462 'default_repo_create_on_write',
463 463 'default_repo_create',
464 464 'default_fork_create',
465 465 'default_inherit_default_permissions',
466 466 'default_branch_perm',
467 467
468 468 'default_register',
469 469 'default_password_reset',
470 470 'default_extern_activate'])
471 471
472 472 # overwrite default repo permissions
473 473 if form_result['overwrite_default_repo']:
474 474 _def_name = form_result['default_repo_perm'].split('repository.')[-1]
475 475 _def = Permission.get_by_key('repository.' + _def_name)
476 476 for r2p in self.sa.query(UserRepoToPerm)\
477 477 .filter(UserRepoToPerm.user == perm_user)\
478 478 .all():
479 479 # don't reset PRIVATE repositories
480 480 if not r2p.repository.private:
481 481 r2p.permission = _def
482 482 self.sa.add(r2p)
483 483
484 484 # overwrite default repo group permissions
485 485 if form_result['overwrite_default_group']:
486 486 _def_name = form_result['default_group_perm'].split('group.')[-1]
487 487 _def = Permission.get_by_key('group.' + _def_name)
488 488 for g2p in self.sa.query(UserRepoGroupToPerm)\
489 489 .filter(UserRepoGroupToPerm.user == perm_user)\
490 490 .all():
491 491 g2p.permission = _def
492 492 self.sa.add(g2p)
493 493
494 494 # overwrite default user group permissions
495 495 if form_result['overwrite_default_user_group']:
496 496 _def_name = form_result['default_user_group_perm'].split('usergroup.')[-1]
497 497 # user groups
498 498 _def = Permission.get_by_key('usergroup.' + _def_name)
499 499 for g2p in self.sa.query(UserUserGroupToPerm)\
500 500 .filter(UserUserGroupToPerm.user == perm_user)\
501 501 .all():
502 502 g2p.permission = _def
503 503 self.sa.add(g2p)
504 504
505 505 # COMMIT
506 506 self.sa.commit()
507 507 except (DatabaseError,):
508 508 log.exception('Failed to set default object permissions')
509 509 self.sa.rollback()
510 510 raise
511 511
512 512 def update_branch_permissions(self, form_result):
513 513 if 'perm_user_id' in form_result:
514 514 perm_user = User.get(safe_int(form_result['perm_user_id']))
515 515 else:
516 516 # used mostly to do lookup for default user
517 517 perm_user = User.get_by_username(form_result['perm_user_name'])
518 518 try:
519 519
520 520 # stage 2 reset defaults and set them from form data
521 521 self._set_new_user_perms(perm_user, form_result, preserve=[
522 522 'default_repo_perm',
523 523 'default_group_perm',
524 524 'default_user_group_perm',
525 525
526 526 'default_repo_group_create',
527 527 'default_user_group_create',
528 528 'default_repo_create_on_write',
529 529 'default_repo_create',
530 530 'default_fork_create',
531 531 'default_inherit_default_permissions',
532 532
533 533 'default_register',
534 534 'default_password_reset',
535 535 'default_extern_activate'])
536 536
537 537 # overwrite default branch permissions
538 538 if form_result['overwrite_default_branch']:
539 539 _def_name = \
540 540 form_result['default_branch_perm'].split('branch.')[-1]
541 541
542 542 _def = Permission.get_by_key('branch.' + _def_name)
543 543
544 544 user_perms = UserToRepoBranchPermission.query()\
545 545 .join(UserToRepoBranchPermission.user_repo_to_perm)\
546 546 .filter(UserRepoToPerm.user == perm_user).all()
547 547
548 548 for g2p in user_perms:
549 549 g2p.permission = _def
550 550 self.sa.add(g2p)
551 551
552 552 # COMMIT
553 553 self.sa.commit()
554 554 except (DatabaseError,):
555 555 log.exception('Failed to set default branch permissions')
556 556 self.sa.rollback()
557 557 raise
558 558
559 559 def get_users_with_repo_write(self, db_repo):
560 560 write_plus = ['repository.write', 'repository.admin']
561 default_user_id = User.get_default_user().user_id
561 default_user_id = User.get_default_user_id()
562 562 user_write_permissions = collections.OrderedDict()
563 563
564 564 # write+ and DEFAULT user for inheritance
565 565 for perm in db_repo.permissions():
566 566 if perm.permission in write_plus or perm.user_id == default_user_id:
567 567 user_write_permissions[perm.user_id] = perm
568 568 return user_write_permissions
569 569
570 570 def get_user_groups_with_repo_write(self, db_repo):
571 571 write_plus = ['repository.write', 'repository.admin']
572 572 user_group_write_permissions = collections.OrderedDict()
573 573
574 574 # write+ and DEFAULT user for inheritance
575 575 for p in db_repo.permission_user_groups():
576 576 if p.permission in write_plus:
577 577 user_group_write_permissions[p.users_group_id] = p
578 578 return user_group_write_permissions
579 579
580 580 def trigger_permission_flush(self, affected_user_ids):
581 581 events.trigger(events.UserPermissionsChange(affected_user_ids))
582 582
583 583 def flush_user_permission_caches(self, changes, affected_user_ids=None):
584 584 affected_user_ids = affected_user_ids or []
585 585
586 586 for change in changes['added'] + changes['updated'] + changes['deleted']:
587 587 if change['type'] == 'user':
588 588 affected_user_ids.append(change['id'])
589 589 if change['type'] == 'user_group':
590 590 user_group = UserGroup.get(safe_int(change['id']))
591 591 if user_group:
592 592 group_members_ids = [x.user_id for x in user_group.members]
593 593 affected_user_ids.extend(group_members_ids)
594 594
595 595 self.trigger_permission_flush(affected_user_ids)
596 596
597 597 return affected_user_ids
@@ -1,1116 +1,1116 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 Set of generic validators
23 23 """
24 24
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import collections
30 30
31 31 import formencode
32 32 import ipaddress
33 33 from formencode.validators import (
34 34 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
35 35 NotEmpty, IPAddress, CIDR, String, FancyValidator
36 36 )
37 37
38 38 from sqlalchemy.sql.expression import true
39 39 from sqlalchemy.util import OrderedSet
40 40 from pyramid import compat
41 41
42 42 from rhodecode.authentication import (
43 43 legacy_plugin_prefix, _import_legacy_plugin)
44 44 from rhodecode.authentication.base import loadplugin
45 45 from rhodecode.apps._base import ADMIN_PREFIX
46 46 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
47 47 from rhodecode.lib.utils import repo_name_slug, make_db_config
48 48 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5, safe_unicode
49 49 from rhodecode.lib.vcs.backends.git.repository import GitRepository
50 50 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
51 51 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
52 52 from rhodecode.model.db import (
53 53 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
54 54 from rhodecode.model.settings import VcsSettingsModel
55 55
56 56 # silence warnings and pylint
57 57 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
58 58 NotEmpty, IPAddress, CIDR, String, FancyValidator
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class _Missing(object):
64 64 pass
65 65
66 66
67 67 Missing = _Missing()
68 68
69 69
70 70 def M(self, key, state, **kwargs):
71 71 """
72 72 returns string from self.message based on given key,
73 73 passed kw params are used to substitute %(named)s params inside
74 74 translated strings
75 75
76 76 :param msg:
77 77 :param state:
78 78 """
79 79
80 80 #state._ = staticmethod(_)
81 81 # inject validator into state object
82 82 return self.message(key, state, **kwargs)
83 83
84 84
85 85 def UniqueList(localizer, convert=None):
86 86 _ = localizer
87 87
88 88 class _validator(formencode.FancyValidator):
89 89 """
90 90 Unique List !
91 91 """
92 92 messages = {
93 93 'empty': _(u'Value cannot be an empty list'),
94 94 'missing_value': _(u'Value cannot be an empty list'),
95 95 }
96 96
97 97 def _to_python(self, value, state):
98 98 ret_val = []
99 99
100 100 def make_unique(value):
101 101 seen = []
102 102 return [c for c in value if not (c in seen or seen.append(c))]
103 103
104 104 if isinstance(value, list):
105 105 ret_val = make_unique(value)
106 106 elif isinstance(value, set):
107 107 ret_val = make_unique(list(value))
108 108 elif isinstance(value, tuple):
109 109 ret_val = make_unique(list(value))
110 110 elif value is None:
111 111 ret_val = []
112 112 else:
113 113 ret_val = [value]
114 114
115 115 if convert:
116 116 ret_val = map(convert, ret_val)
117 117 return ret_val
118 118
119 119 def empty_value(self, value):
120 120 return []
121 121 return _validator
122 122
123 123
124 124 def UniqueListFromString(localizer):
125 125 _ = localizer
126 126
127 127 class _validator(UniqueList(localizer)):
128 128 def _to_python(self, value, state):
129 129 if isinstance(value, compat.string_types):
130 130 value = aslist(value, ',')
131 131 return super(_validator, self)._to_python(value, state)
132 132 return _validator
133 133
134 134
135 135 def ValidSvnPattern(localizer, section, repo_name=None):
136 136 _ = localizer
137 137
138 138 class _validator(formencode.validators.FancyValidator):
139 139 messages = {
140 140 'pattern_exists': _(u'Pattern already exists'),
141 141 }
142 142
143 143 def validate_python(self, value, state):
144 144 if not value:
145 145 return
146 146 model = VcsSettingsModel(repo=repo_name)
147 147 ui_settings = model.get_svn_patterns(section=section)
148 148 for entry in ui_settings:
149 149 if value == entry.value:
150 150 msg = M(self, 'pattern_exists', state)
151 151 raise formencode.Invalid(msg, value, state)
152 152 return _validator
153 153
154 154
155 155 def ValidUsername(localizer, edit=False, old_data=None):
156 156 _ = localizer
157 157 old_data = old_data or {}
158 158
159 159 class _validator(formencode.validators.FancyValidator):
160 160 messages = {
161 161 'username_exists': _(u'Username "%(username)s" already exists'),
162 162 'system_invalid_username':
163 163 _(u'Username "%(username)s" is forbidden'),
164 164 'invalid_username':
165 165 _(u'Username may only contain alphanumeric characters '
166 166 u'underscores, periods or dashes and must begin with '
167 167 u'alphanumeric character or underscore')
168 168 }
169 169
170 170 def validate_python(self, value, state):
171 171 if value in ['default', 'new_user']:
172 172 msg = M(self, 'system_invalid_username', state, username=value)
173 173 raise formencode.Invalid(msg, value, state)
174 174 # check if user is unique
175 175 old_un = None
176 176 if edit:
177 177 old_un = User.get(old_data.get('user_id')).username
178 178
179 179 if old_un != value or not edit:
180 180 if User.get_by_username(value, case_insensitive=True):
181 181 msg = M(self, 'username_exists', state, username=value)
182 182 raise formencode.Invalid(msg, value, state)
183 183
184 184 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
185 185 is None):
186 186 msg = M(self, 'invalid_username', state)
187 187 raise formencode.Invalid(msg, value, state)
188 188 return _validator
189 189
190 190
191 191 def ValidRepoUser(localizer, allow_disabled=False):
192 192 _ = localizer
193 193
194 194 class _validator(formencode.validators.FancyValidator):
195 195 messages = {
196 196 'invalid_username': _(u'Username %(username)s is not valid'),
197 197 'disabled_username': _(u'Username %(username)s is disabled')
198 198 }
199 199
200 200 def validate_python(self, value, state):
201 201 try:
202 202 user = User.query().filter(User.username == value).one()
203 203 except Exception:
204 204 msg = M(self, 'invalid_username', state, username=value)
205 205 raise formencode.Invalid(
206 206 msg, value, state, error_dict={'username': msg}
207 207 )
208 208 if user and (not allow_disabled and not user.active):
209 209 msg = M(self, 'disabled_username', state, username=value)
210 210 raise formencode.Invalid(
211 211 msg, value, state, error_dict={'username': msg}
212 212 )
213 213 return _validator
214 214
215 215
216 216 def ValidUserGroup(localizer, edit=False, old_data=None):
217 217 _ = localizer
218 218 old_data = old_data or {}
219 219
220 220 class _validator(formencode.validators.FancyValidator):
221 221 messages = {
222 222 'invalid_group': _(u'Invalid user group name'),
223 223 'group_exist': _(u'User group `%(usergroup)s` already exists'),
224 224 'invalid_usergroup_name':
225 225 _(u'user group name may only contain alphanumeric '
226 226 u'characters underscores, periods or dashes and must begin '
227 227 u'with alphanumeric character')
228 228 }
229 229
230 230 def validate_python(self, value, state):
231 231 if value in ['default']:
232 232 msg = M(self, 'invalid_group', state)
233 233 raise formencode.Invalid(
234 234 msg, value, state, error_dict={'users_group_name': msg}
235 235 )
236 236 # check if group is unique
237 237 old_ugname = None
238 238 if edit:
239 239 old_id = old_data.get('users_group_id')
240 240 old_ugname = UserGroup.get(old_id).users_group_name
241 241
242 242 if old_ugname != value or not edit:
243 243 is_existing_group = UserGroup.get_by_group_name(
244 244 value, case_insensitive=True)
245 245 if is_existing_group:
246 246 msg = M(self, 'group_exist', state, usergroup=value)
247 247 raise formencode.Invalid(
248 248 msg, value, state, error_dict={'users_group_name': msg}
249 249 )
250 250
251 251 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
252 252 msg = M(self, 'invalid_usergroup_name', state)
253 253 raise formencode.Invalid(
254 254 msg, value, state, error_dict={'users_group_name': msg}
255 255 )
256 256 return _validator
257 257
258 258
259 259 def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=False):
260 260 _ = localizer
261 261 old_data = old_data or {}
262 262
263 263 class _validator(formencode.validators.FancyValidator):
264 264 messages = {
265 265 'group_parent_id': _(u'Cannot assign this group as parent'),
266 266 'group_exists': _(u'Group "%(group_name)s" already exists'),
267 267 'repo_exists': _(u'Repository with name "%(group_name)s" '
268 268 u'already exists'),
269 269 'permission_denied': _(u"no permission to store repository group"
270 270 u"in this location"),
271 271 'permission_denied_root': _(
272 272 u"no permission to store repository group "
273 273 u"in root location")
274 274 }
275 275
276 276 def _to_python(self, value, state):
277 277 group_name = repo_name_slug(value.get('group_name', ''))
278 278 group_parent_id = safe_int(value.get('group_parent_id'))
279 279 gr = RepoGroup.get(group_parent_id)
280 280 if gr:
281 281 parent_group_path = gr.full_path
282 282 # value needs to be aware of group name in order to check
283 283 # db key This is an actual just the name to store in the
284 284 # database
285 285 group_name_full = (
286 286 parent_group_path + RepoGroup.url_sep() + group_name)
287 287 else:
288 288 group_name_full = group_name
289 289
290 290 value['group_name'] = group_name
291 291 value['group_name_full'] = group_name_full
292 292 value['group_parent_id'] = group_parent_id
293 293 return value
294 294
295 295 def validate_python(self, value, state):
296 296
297 297 old_group_name = None
298 298 group_name = value.get('group_name')
299 299 group_name_full = value.get('group_name_full')
300 300 group_parent_id = safe_int(value.get('group_parent_id'))
301 301 if group_parent_id == -1:
302 302 group_parent_id = None
303 303
304 304 group_obj = RepoGroup.get(old_data.get('group_id'))
305 305 parent_group_changed = False
306 306 if edit:
307 307 old_group_name = group_obj.group_name
308 308 old_group_parent_id = group_obj.group_parent_id
309 309
310 310 if group_parent_id != old_group_parent_id:
311 311 parent_group_changed = True
312 312
313 313 # TODO: mikhail: the following if statement is not reached
314 314 # since group_parent_id's OneOf validation fails before.
315 315 # Can be removed.
316 316
317 317 # check against setting a parent of self
318 318 parent_of_self = (
319 319 old_data['group_id'] == group_parent_id
320 320 if group_parent_id else False
321 321 )
322 322 if parent_of_self:
323 323 msg = M(self, 'group_parent_id', state)
324 324 raise formencode.Invalid(
325 325 msg, value, state, error_dict={'group_parent_id': msg}
326 326 )
327 327
328 328 # group we're moving current group inside
329 329 child_group = None
330 330 if group_parent_id:
331 331 child_group = RepoGroup.query().filter(
332 332 RepoGroup.group_id == group_parent_id).scalar()
333 333
334 334 # do a special check that we cannot move a group to one of
335 335 # it's children
336 336 if edit and child_group:
337 337 parents = [x.group_id for x in child_group.parents]
338 338 move_to_children = old_data['group_id'] in parents
339 339 if move_to_children:
340 340 msg = M(self, 'group_parent_id', state)
341 341 raise formencode.Invalid(
342 342 msg, value, state, error_dict={'group_parent_id': msg})
343 343
344 344 # Check if we have permission to store in the parent.
345 345 # Only check if the parent group changed.
346 346 if parent_group_changed:
347 347 if child_group is None:
348 348 if not can_create_in_root:
349 349 msg = M(self, 'permission_denied_root', state)
350 350 raise formencode.Invalid(
351 351 msg, value, state,
352 352 error_dict={'group_parent_id': msg})
353 353 else:
354 354 valid = HasRepoGroupPermissionAny('group.admin')
355 355 forbidden = not valid(
356 356 child_group.group_name, 'can create group validator')
357 357 if forbidden:
358 358 msg = M(self, 'permission_denied', state)
359 359 raise formencode.Invalid(
360 360 msg, value, state,
361 361 error_dict={'group_parent_id': msg})
362 362
363 363 # if we change the name or it's new group, check for existing names
364 364 # or repositories with the same name
365 365 if old_group_name != group_name_full or not edit:
366 366 # check group
367 367 gr = RepoGroup.get_by_group_name(group_name_full)
368 368 if gr:
369 369 msg = M(self, 'group_exists', state, group_name=group_name)
370 370 raise formencode.Invalid(
371 371 msg, value, state, error_dict={'group_name': msg})
372 372
373 373 # check for same repo
374 374 repo = Repository.get_by_repo_name(group_name_full)
375 375 if repo:
376 376 msg = M(self, 'repo_exists', state, group_name=group_name)
377 377 raise formencode.Invalid(
378 378 msg, value, state, error_dict={'group_name': msg})
379 379 return _validator
380 380
381 381
382 382 def ValidPassword(localizer):
383 383 _ = localizer
384 384
385 385 class _validator(formencode.validators.FancyValidator):
386 386 messages = {
387 387 'invalid_password':
388 388 _(u'Invalid characters (non-ascii) in password')
389 389 }
390 390
391 391 def validate_python(self, value, state):
392 392 try:
393 393 (value or '').decode('ascii')
394 394 except UnicodeError:
395 395 msg = M(self, 'invalid_password', state)
396 396 raise formencode.Invalid(msg, value, state,)
397 397 return _validator
398 398
399 399
400 400 def ValidPasswordsMatch(
401 401 localizer, passwd='new_password',
402 402 passwd_confirmation='password_confirmation'):
403 403 _ = localizer
404 404
405 405 class _validator(formencode.validators.FancyValidator):
406 406 messages = {
407 407 'password_mismatch': _(u'Passwords do not match'),
408 408 }
409 409
410 410 def validate_python(self, value, state):
411 411
412 412 pass_val = value.get('password') or value.get(passwd)
413 413 if pass_val != value[passwd_confirmation]:
414 414 msg = M(self, 'password_mismatch', state)
415 415 raise formencode.Invalid(
416 416 msg, value, state,
417 417 error_dict={passwd: msg, passwd_confirmation: msg}
418 418 )
419 419 return _validator
420 420
421 421
422 422 def ValidAuth(localizer):
423 423 _ = localizer
424 424
425 425 class _validator(formencode.validators.FancyValidator):
426 426 messages = {
427 427 'invalid_password': _(u'invalid password'),
428 428 'invalid_username': _(u'invalid user name'),
429 429 'disabled_account': _(u'Your account is disabled')
430 430 }
431 431
432 432 def validate_python(self, value, state):
433 433 from rhodecode.authentication.base import authenticate, HTTP_TYPE
434 434
435 435 password = value['password']
436 436 username = value['username']
437 437
438 438 if not authenticate(username, password, '', HTTP_TYPE,
439 439 skip_missing=True):
440 440 user = User.get_by_username(username)
441 441 if user and not user.active:
442 442 log.warning('user %s is disabled', username)
443 443 msg = M(self, 'disabled_account', state)
444 444 raise formencode.Invalid(
445 445 msg, value, state, error_dict={'username': msg}
446 446 )
447 447 else:
448 448 log.warning('user `%s` failed to authenticate', username)
449 449 msg = M(self, 'invalid_username', state)
450 450 msg2 = M(self, 'invalid_password', state)
451 451 raise formencode.Invalid(
452 452 msg, value, state,
453 453 error_dict={'username': msg, 'password': msg2}
454 454 )
455 455 return _validator
456 456
457 457
458 458 def ValidRepoName(localizer, edit=False, old_data=None):
459 459 old_data = old_data or {}
460 460 _ = localizer
461 461
462 462 class _validator(formencode.validators.FancyValidator):
463 463 messages = {
464 464 'invalid_repo_name':
465 465 _(u'Repository name %(repo)s is disallowed'),
466 466 # top level
467 467 'repository_exists': _(u'Repository with name %(repo)s '
468 468 u'already exists'),
469 469 'group_exists': _(u'Repository group with name "%(repo)s" '
470 470 u'already exists'),
471 471 # inside a group
472 472 'repository_in_group_exists': _(u'Repository with name %(repo)s '
473 473 u'exists in group "%(group)s"'),
474 474 'group_in_group_exists': _(
475 475 u'Repository group with name "%(repo)s" '
476 476 u'exists in group "%(group)s"'),
477 477 }
478 478
479 479 def _to_python(self, value, state):
480 480 repo_name = repo_name_slug(value.get('repo_name', ''))
481 481 repo_group = value.get('repo_group')
482 482 if repo_group:
483 483 gr = RepoGroup.get(repo_group)
484 484 group_path = gr.full_path
485 485 group_name = gr.group_name
486 486 # value needs to be aware of group name in order to check
487 487 # db key This is an actual just the name to store in the
488 488 # database
489 489 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
490 490 else:
491 491 group_name = group_path = ''
492 492 repo_name_full = repo_name
493 493
494 494 value['repo_name'] = repo_name
495 495 value['repo_name_full'] = repo_name_full
496 496 value['group_path'] = group_path
497 497 value['group_name'] = group_name
498 498 return value
499 499
500 500 def validate_python(self, value, state):
501 501
502 502 repo_name = value.get('repo_name')
503 503 repo_name_full = value.get('repo_name_full')
504 504 group_path = value.get('group_path')
505 505 group_name = value.get('group_name')
506 506
507 507 if repo_name in [ADMIN_PREFIX, '']:
508 508 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
509 509 raise formencode.Invalid(
510 510 msg, value, state, error_dict={'repo_name': msg})
511 511
512 512 rename = old_data.get('repo_name') != repo_name_full
513 513 create = not edit
514 514 if rename or create:
515 515
516 516 if group_path:
517 517 if Repository.get_by_repo_name(repo_name_full):
518 518 msg = M(self, 'repository_in_group_exists', state,
519 519 repo=repo_name, group=group_name)
520 520 raise formencode.Invalid(
521 521 msg, value, state, error_dict={'repo_name': msg})
522 522 if RepoGroup.get_by_group_name(repo_name_full):
523 523 msg = M(self, 'group_in_group_exists', state,
524 524 repo=repo_name, group=group_name)
525 525 raise formencode.Invalid(
526 526 msg, value, state, error_dict={'repo_name': msg})
527 527 else:
528 528 if RepoGroup.get_by_group_name(repo_name_full):
529 529 msg = M(self, 'group_exists', state, repo=repo_name)
530 530 raise formencode.Invalid(
531 531 msg, value, state, error_dict={'repo_name': msg})
532 532
533 533 if Repository.get_by_repo_name(repo_name_full):
534 534 msg = M(
535 535 self, 'repository_exists', state, repo=repo_name)
536 536 raise formencode.Invalid(
537 537 msg, value, state, error_dict={'repo_name': msg})
538 538 return value
539 539 return _validator
540 540
541 541
542 542 def ValidForkName(localizer, *args, **kwargs):
543 543 _ = localizer
544 544
545 545 return ValidRepoName(localizer, *args, **kwargs)
546 546
547 547
548 548 def SlugifyName(localizer):
549 549 _ = localizer
550 550
551 551 class _validator(formencode.validators.FancyValidator):
552 552
553 553 def _to_python(self, value, state):
554 554 return repo_name_slug(value)
555 555
556 556 def validate_python(self, value, state):
557 557 pass
558 558 return _validator
559 559
560 560
561 561 def CannotHaveGitSuffix(localizer):
562 562 _ = localizer
563 563
564 564 class _validator(formencode.validators.FancyValidator):
565 565 messages = {
566 566 'has_git_suffix':
567 567 _(u'Repository name cannot end with .git'),
568 568 }
569 569
570 570 def _to_python(self, value, state):
571 571 return value
572 572
573 573 def validate_python(self, value, state):
574 574 if value and value.endswith('.git'):
575 575 msg = M(
576 576 self, 'has_git_suffix', state)
577 577 raise formencode.Invalid(
578 578 msg, value, state, error_dict={'repo_name': msg})
579 579 return _validator
580 580
581 581
582 582 def ValidCloneUri(localizer):
583 583 _ = localizer
584 584
585 585 class InvalidCloneUrl(Exception):
586 586 allowed_prefixes = ()
587 587
588 588 def url_handler(repo_type, url):
589 589 config = make_db_config(clear_session=False)
590 590 if repo_type == 'hg':
591 591 allowed_prefixes = ('http', 'svn+http', 'git+http')
592 592
593 593 if 'http' in url[:4]:
594 594 # initially check if it's at least the proper URL
595 595 # or does it pass basic auth
596 596 MercurialRepository.check_url(url, config)
597 597 elif 'svn+http' in url[:8]: # svn->hg import
598 598 SubversionRepository.check_url(url, config)
599 599 elif 'git+http' in url[:8]: # git->hg import
600 600 raise NotImplementedError()
601 601 else:
602 602 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
603 603 'Allowed url must start with one of %s'
604 604 % (url, ','.join(allowed_prefixes)))
605 605 exc.allowed_prefixes = allowed_prefixes
606 606 raise exc
607 607
608 608 elif repo_type == 'git':
609 609 allowed_prefixes = ('http', 'svn+http', 'hg+http')
610 610 if 'http' in url[:4]:
611 611 # initially check if it's at least the proper URL
612 612 # or does it pass basic auth
613 613 GitRepository.check_url(url, config)
614 614 elif 'svn+http' in url[:8]: # svn->git import
615 615 raise NotImplementedError()
616 616 elif 'hg+http' in url[:8]: # hg->git import
617 617 raise NotImplementedError()
618 618 else:
619 619 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
620 620 'Allowed url must start with one of %s'
621 621 % (url, ','.join(allowed_prefixes)))
622 622 exc.allowed_prefixes = allowed_prefixes
623 623 raise exc
624 624
625 625 class _validator(formencode.validators.FancyValidator):
626 626 messages = {
627 627 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
628 628 'invalid_clone_uri': _(
629 629 u'Invalid clone url, provide a valid clone '
630 630 u'url starting with one of %(allowed_prefixes)s')
631 631 }
632 632
633 633 def validate_python(self, value, state):
634 634 repo_type = value.get('repo_type')
635 635 url = value.get('clone_uri')
636 636
637 637 if url:
638 638 try:
639 639 url_handler(repo_type, url)
640 640 except InvalidCloneUrl as e:
641 641 log.warning(e)
642 642 msg = M(self, 'invalid_clone_uri', state, rtype=repo_type,
643 643 allowed_prefixes=','.join(e.allowed_prefixes))
644 644 raise formencode.Invalid(msg, value, state,
645 645 error_dict={'clone_uri': msg})
646 646 except Exception:
647 647 log.exception('Url validation failed')
648 648 msg = M(self, 'clone_uri', state, rtype=repo_type)
649 649 raise formencode.Invalid(msg, value, state,
650 650 error_dict={'clone_uri': msg})
651 651 return _validator
652 652
653 653
654 654 def ValidForkType(localizer, old_data=None):
655 655 _ = localizer
656 656 old_data = old_data or {}
657 657
658 658 class _validator(formencode.validators.FancyValidator):
659 659 messages = {
660 660 'invalid_fork_type': _(u'Fork have to be the same type as parent')
661 661 }
662 662
663 663 def validate_python(self, value, state):
664 664 if old_data['repo_type'] != value:
665 665 msg = M(self, 'invalid_fork_type', state)
666 666 raise formencode.Invalid(
667 667 msg, value, state, error_dict={'repo_type': msg}
668 668 )
669 669 return _validator
670 670
671 671
672 672 def CanWriteGroup(localizer, old_data=None):
673 673 _ = localizer
674 674
675 675 class _validator(formencode.validators.FancyValidator):
676 676 messages = {
677 677 'permission_denied': _(
678 678 u"You do not have the permission "
679 679 u"to create repositories in this group."),
680 680 'permission_denied_root': _(
681 681 u"You do not have the permission to store repositories in "
682 682 u"the root location.")
683 683 }
684 684
685 685 def _to_python(self, value, state):
686 686 # root location
687 687 if value in [-1, "-1"]:
688 688 return None
689 689 return value
690 690
691 691 def validate_python(self, value, state):
692 692 gr = RepoGroup.get(value)
693 693 gr_name = gr.group_name if gr else None # None means ROOT location
694 694 # create repositories with write permission on group is set to true
695 695 create_on_write = HasPermissionAny(
696 696 'hg.create.write_on_repogroup.true')()
697 697 group_admin = HasRepoGroupPermissionAny('group.admin')(
698 698 gr_name, 'can write into group validator')
699 699 group_write = HasRepoGroupPermissionAny('group.write')(
700 700 gr_name, 'can write into group validator')
701 701 forbidden = not (group_admin or (group_write and create_on_write))
702 702 can_create_repos = HasPermissionAny(
703 703 'hg.admin', 'hg.create.repository')
704 704 gid = (old_data['repo_group'].get('group_id')
705 705 if (old_data and 'repo_group' in old_data) else None)
706 706 value_changed = gid != safe_int(value)
707 707 new = not old_data
708 708 # do check if we changed the value, there's a case that someone got
709 709 # revoked write permissions to a repository, he still created, we
710 710 # don't need to check permission if he didn't change the value of
711 711 # groups in form box
712 712 if value_changed or new:
713 713 # parent group need to be existing
714 714 if gr and forbidden:
715 715 msg = M(self, 'permission_denied', state)
716 716 raise formencode.Invalid(
717 717 msg, value, state, error_dict={'repo_type': msg}
718 718 )
719 719 # check if we can write to root location !
720 720 elif gr is None and not can_create_repos():
721 721 msg = M(self, 'permission_denied_root', state)
722 722 raise formencode.Invalid(
723 723 msg, value, state, error_dict={'repo_type': msg}
724 724 )
725 725 return _validator
726 726
727 727
728 728 def ValidPerms(localizer, type_='repo'):
729 729 _ = localizer
730 730 if type_ == 'repo_group':
731 731 EMPTY_PERM = 'group.none'
732 732 elif type_ == 'repo':
733 733 EMPTY_PERM = 'repository.none'
734 734 elif type_ == 'user_group':
735 735 EMPTY_PERM = 'usergroup.none'
736 736
737 737 class _validator(formencode.validators.FancyValidator):
738 738 messages = {
739 739 'perm_new_member_name':
740 740 _(u'This username or user group name is not valid')
741 741 }
742 742
743 743 def _to_python(self, value, state):
744 744 perm_updates = OrderedSet()
745 745 perm_additions = OrderedSet()
746 746 perm_deletions = OrderedSet()
747 747 # build a list of permission to update/delete and new permission
748 748
749 749 # Read the perm_new_member/perm_del_member attributes and group
750 750 # them by they IDs
751 751 new_perms_group = collections.defaultdict(dict)
752 752 del_perms_group = collections.defaultdict(dict)
753 753 for k, v in value.copy().iteritems():
754 754 if k.startswith('perm_del_member'):
755 755 # delete from org storage so we don't process that later
756 756 del value[k]
757 757 # part is `id`, `type`
758 758 _type, part = k.split('perm_del_member_')
759 759 args = part.split('_')
760 760 if len(args) == 2:
761 761 _key, pos = args
762 762 del_perms_group[pos][_key] = v
763 763 if k.startswith('perm_new_member'):
764 764 # delete from org storage so we don't process that later
765 765 del value[k]
766 766 # part is `id`, `type`, `perm`
767 767 _type, part = k.split('perm_new_member_')
768 768 args = part.split('_')
769 769 if len(args) == 2:
770 770 _key, pos = args
771 771 new_perms_group[pos][_key] = v
772 772
773 773 # store the deletes
774 774 for k in sorted(del_perms_group.keys()):
775 775 perm_dict = del_perms_group[k]
776 776 del_member = perm_dict.get('id')
777 777 del_type = perm_dict.get('type')
778 778 if del_member and del_type:
779 779 perm_deletions.add(
780 780 (del_member, None, del_type))
781 781
782 782 # store additions in order of how they were added in web form
783 783 for k in sorted(new_perms_group.keys()):
784 784 perm_dict = new_perms_group[k]
785 785 new_member = perm_dict.get('id')
786 786 new_type = perm_dict.get('type')
787 787 new_perm = perm_dict.get('perm')
788 788 if new_member and new_perm and new_type:
789 789 perm_additions.add(
790 790 (new_member, new_perm, new_type))
791 791
792 792 # get updates of permissions
793 793 # (read the existing radio button states)
794 default_user_id = User.get_default_user().user_id
794 default_user_id = User.get_default_user_id()
795 795
796 796 for k, update_value in value.iteritems():
797 797 if k.startswith('u_perm_') or k.startswith('g_perm_'):
798 798 obj_type = k[0]
799 799 obj_id = k[7:]
800 800 update_type = {'u': 'user',
801 801 'g': 'user_group'}[obj_type]
802 802
803 803 if obj_type == 'u' and safe_int(obj_id) == default_user_id:
804 804 if str2bool(value.get('repo_private')):
805 805 # prevent from updating default user permissions
806 806 # when this repository is marked as private
807 807 update_value = EMPTY_PERM
808 808
809 809 perm_updates.add(
810 810 (obj_id, update_value, update_type))
811 811
812 812 value['perm_additions'] = [] # propagated later
813 813 value['perm_updates'] = list(perm_updates)
814 814 value['perm_deletions'] = list(perm_deletions)
815 815
816 816 updates_map = dict(
817 817 (x[0], (x[1], x[2])) for x in value['perm_updates'])
818 818 # make sure Additions don't override updates.
819 819 for member_id, perm, member_type in list(perm_additions):
820 820 if member_id in updates_map:
821 821 perm = updates_map[member_id][0]
822 822 value['perm_additions'].append((member_id, perm, member_type))
823 823
824 824 # on new entries validate users they exist and they are active !
825 825 # this leaves feedback to the form
826 826 try:
827 827 if member_type == 'user':
828 828 User.query()\
829 829 .filter(User.active == true())\
830 830 .filter(User.user_id == member_id).one()
831 831 if member_type == 'user_group':
832 832 UserGroup.query()\
833 833 .filter(UserGroup.users_group_active == true())\
834 834 .filter(UserGroup.users_group_id == member_id)\
835 835 .one()
836 836
837 837 except Exception:
838 838 log.exception('Updated permission failed: org_exc:')
839 839 msg = M(self, 'perm_new_member_type', state)
840 840 raise formencode.Invalid(
841 841 msg, value, state, error_dict={
842 842 'perm_new_member_name': msg}
843 843 )
844 844 return value
845 845 return _validator
846 846
847 847
848 848 def ValidPath(localizer):
849 849 _ = localizer
850 850
851 851 class _validator(formencode.validators.FancyValidator):
852 852 messages = {
853 853 'invalid_path': _(u'This is not a valid path')
854 854 }
855 855
856 856 def validate_python(self, value, state):
857 857 if not os.path.isdir(value):
858 858 msg = M(self, 'invalid_path', state)
859 859 raise formencode.Invalid(
860 860 msg, value, state, error_dict={'paths_root_path': msg}
861 861 )
862 862 return _validator
863 863
864 864
865 865 def UniqSystemEmail(localizer, old_data=None):
866 866 _ = localizer
867 867 old_data = old_data or {}
868 868
869 869 class _validator(formencode.validators.FancyValidator):
870 870 messages = {
871 871 'email_taken': _(u'This e-mail address is already taken')
872 872 }
873 873
874 874 def _to_python(self, value, state):
875 875 return value.lower()
876 876
877 877 def validate_python(self, value, state):
878 878 if (old_data.get('email') or '').lower() != value:
879 879 user = User.get_by_email(value, case_insensitive=True)
880 880 if user:
881 881 msg = M(self, 'email_taken', state)
882 882 raise formencode.Invalid(
883 883 msg, value, state, error_dict={'email': msg}
884 884 )
885 885 return _validator
886 886
887 887
888 888 def ValidSystemEmail(localizer):
889 889 _ = localizer
890 890
891 891 class _validator(formencode.validators.FancyValidator):
892 892 messages = {
893 893 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
894 894 }
895 895
896 896 def _to_python(self, value, state):
897 897 return value.lower()
898 898
899 899 def validate_python(self, value, state):
900 900 user = User.get_by_email(value, case_insensitive=True)
901 901 if user is None:
902 902 msg = M(self, 'non_existing_email', state, email=value)
903 903 raise formencode.Invalid(
904 904 msg, value, state, error_dict={'email': msg}
905 905 )
906 906 return _validator
907 907
908 908
909 909 def NotReviewedRevisions(localizer, repo_id):
910 910 _ = localizer
911 911 class _validator(formencode.validators.FancyValidator):
912 912 messages = {
913 913 'rev_already_reviewed':
914 914 _(u'Revisions %(revs)s are already part of pull request '
915 915 u'or have set status'),
916 916 }
917 917
918 918 def validate_python(self, value, state):
919 919 # check revisions if they are not reviewed, or a part of another
920 920 # pull request
921 921 statuses = ChangesetStatus.query()\
922 922 .filter(ChangesetStatus.revision.in_(value))\
923 923 .filter(ChangesetStatus.repo_id == repo_id)\
924 924 .all()
925 925
926 926 errors = []
927 927 for status in statuses:
928 928 if status.pull_request_id:
929 929 errors.append(['pull_req', status.revision[:12]])
930 930 elif status.status:
931 931 errors.append(['status', status.revision[:12]])
932 932
933 933 if errors:
934 934 revs = ','.join([x[1] for x in errors])
935 935 msg = M(self, 'rev_already_reviewed', state, revs=revs)
936 936 raise formencode.Invalid(
937 937 msg, value, state, error_dict={'revisions': revs})
938 938
939 939 return _validator
940 940
941 941
942 942 def ValidIp(localizer):
943 943 _ = localizer
944 944
945 945 class _validator(CIDR):
946 946 messages = {
947 947 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
948 948 'illegalBits': _(
949 949 u'The network size (bits) must be within the range '
950 950 u'of 0-32 (not %(bits)r)'),
951 951 }
952 952
953 953 # we ovveride the default to_python() call
954 954 def to_python(self, value, state):
955 955 v = super(_validator, self).to_python(value, state)
956 956 v = safe_unicode(v.strip())
957 957 net = ipaddress.ip_network(address=v, strict=False)
958 958 return str(net)
959 959
960 960 def validate_python(self, value, state):
961 961 try:
962 962 addr = safe_unicode(value.strip())
963 963 # this raises an ValueError if address is not IpV4 or IpV6
964 964 ipaddress.ip_network(addr, strict=False)
965 965 except ValueError:
966 966 raise formencode.Invalid(self.message('badFormat', state),
967 967 value, state)
968 968 return _validator
969 969
970 970
971 971 def FieldKey(localizer):
972 972 _ = localizer
973 973
974 974 class _validator(formencode.validators.FancyValidator):
975 975 messages = {
976 976 'badFormat': _(
977 977 u'Key name can only consist of letters, '
978 978 u'underscore, dash or numbers'),
979 979 }
980 980
981 981 def validate_python(self, value, state):
982 982 if not re.match('[a-zA-Z0-9_-]+$', value):
983 983 raise formencode.Invalid(self.message('badFormat', state),
984 984 value, state)
985 985 return _validator
986 986
987 987
988 988 def ValidAuthPlugins(localizer):
989 989 _ = localizer
990 990
991 991 class _validator(formencode.validators.FancyValidator):
992 992 messages = {
993 993 'import_duplicate': _(
994 994 u'Plugins %(loaded)s and %(next_to_load)s '
995 995 u'both export the same name'),
996 996 'missing_includeme': _(
997 997 u'The plugin "%(plugin_id)s" is missing an includeme '
998 998 u'function.'),
999 999 'import_error': _(
1000 1000 u'Can not load plugin "%(plugin_id)s"'),
1001 1001 'no_plugin': _(
1002 1002 u'No plugin available with ID "%(plugin_id)s"'),
1003 1003 }
1004 1004
1005 1005 def _to_python(self, value, state):
1006 1006 # filter empty values
1007 1007 return filter(lambda s: s not in [None, ''], value)
1008 1008
1009 1009 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1010 1010 """
1011 1011 Validates that the plugin import works. It also checks that the
1012 1012 plugin has an includeme attribute.
1013 1013 """
1014 1014 try:
1015 1015 plugin = _import_legacy_plugin(plugin_id)
1016 1016 except Exception as e:
1017 1017 log.exception(
1018 1018 'Exception during import of auth legacy plugin "{}"'
1019 1019 .format(plugin_id))
1020 1020 msg = M(self, 'import_error', state, plugin_id=plugin_id)
1021 1021 raise formencode.Invalid(msg, value, state)
1022 1022
1023 1023 if not hasattr(plugin, 'includeme'):
1024 1024 msg = M(self, 'missing_includeme', state, plugin_id=plugin_id)
1025 1025 raise formencode.Invalid(msg, value, state)
1026 1026
1027 1027 return plugin
1028 1028
1029 1029 def _validate_plugin_id(self, plugin_id, value, state):
1030 1030 """
1031 1031 Plugins are already imported during app start up. Therefore this
1032 1032 validation only retrieves the plugin from the plugin registry and
1033 1033 if it returns something not None everything is OK.
1034 1034 """
1035 1035 plugin = loadplugin(plugin_id)
1036 1036
1037 1037 if plugin is None:
1038 1038 msg = M(self, 'no_plugin', state, plugin_id=plugin_id)
1039 1039 raise formencode.Invalid(msg, value, state)
1040 1040
1041 1041 return plugin
1042 1042
1043 1043 def validate_python(self, value, state):
1044 1044 unique_names = {}
1045 1045 for plugin_id in value:
1046 1046
1047 1047 # Validate legacy or normal plugin.
1048 1048 if plugin_id.startswith(legacy_plugin_prefix):
1049 1049 plugin = self._validate_legacy_plugin_id(
1050 1050 plugin_id, value, state)
1051 1051 else:
1052 1052 plugin = self._validate_plugin_id(plugin_id, value, state)
1053 1053
1054 1054 # Only allow unique plugin names.
1055 1055 if plugin.name in unique_names:
1056 1056 msg = M(self, 'import_duplicate', state,
1057 1057 loaded=unique_names[plugin.name],
1058 1058 next_to_load=plugin)
1059 1059 raise formencode.Invalid(msg, value, state)
1060 1060 unique_names[plugin.name] = plugin
1061 1061 return _validator
1062 1062
1063 1063
1064 1064 def ValidPattern(localizer):
1065 1065 _ = localizer
1066 1066
1067 1067 class _validator(formencode.validators.FancyValidator):
1068 1068 messages = {
1069 1069 'bad_format': _(u'Url must start with http or /'),
1070 1070 }
1071 1071
1072 1072 def _to_python(self, value, state):
1073 1073 patterns = []
1074 1074
1075 1075 prefix = 'new_pattern'
1076 1076 for name, v in value.iteritems():
1077 1077 pattern_name = '_'.join((prefix, 'pattern'))
1078 1078 if name.startswith(pattern_name):
1079 1079 new_item_id = name[len(pattern_name)+1:]
1080 1080
1081 1081 def _field(name):
1082 1082 return '%s_%s_%s' % (prefix, name, new_item_id)
1083 1083
1084 1084 values = {
1085 1085 'issuetracker_pat': value.get(_field('pattern')),
1086 1086 'issuetracker_url': value.get(_field('url')),
1087 1087 'issuetracker_pref': value.get(_field('prefix')),
1088 1088 'issuetracker_desc': value.get(_field('description'))
1089 1089 }
1090 1090 new_uid = md5(values['issuetracker_pat'])
1091 1091
1092 1092 has_required_fields = (
1093 1093 values['issuetracker_pat']
1094 1094 and values['issuetracker_url'])
1095 1095
1096 1096 if has_required_fields:
1097 1097 # validate url that it starts with http or /
1098 1098 # otherwise it can lead to JS injections
1099 1099 # e.g specifig javascript:<malicios code>
1100 1100 if not values['issuetracker_url'].startswith(('http', '/')):
1101 1101 raise formencode.Invalid(
1102 1102 self.message('bad_format', state),
1103 1103 value, state)
1104 1104
1105 1105 settings = [
1106 1106 ('_'.join((key, new_uid)), values[key], 'unicode')
1107 1107 for key in values]
1108 1108 patterns.append(settings)
1109 1109
1110 1110 value['patterns'] = patterns
1111 1111 delete_patterns = value.get('uid') or []
1112 1112 if not isinstance(delete_patterns, (list, tuple)):
1113 1113 delete_patterns = [delete_patterns]
1114 1114 value['delete_patterns'] = delete_patterns
1115 1115 return value
1116 1116 return _validator
@@ -1,632 +1,632 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 os
22 22 from hashlib import sha1
23 23
24 24 import pytest
25 25 from mock import patch
26 26
27 27 from rhodecode.lib import auth
28 28 from rhodecode.lib.utils2 import md5
29 29 from rhodecode.model.auth_token import AuthTokenModel
30 30 from rhodecode.model.db import Session, User
31 31 from rhodecode.model.repo import RepoModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.model.user_group import UserGroupModel
34 34
35 35
36 36 def test_perm_origin_dict():
37 37 pod = auth.PermOriginDict()
38 38 pod['thing'] = 'read', 'default', 1
39 39 assert pod['thing'] == 'read'
40 40
41 41 assert pod.perm_origin_stack == {
42 42 'thing': [('read', 'default', 1)]}
43 43
44 44 pod['thing'] = 'write', 'admin', 1
45 45 assert pod['thing'] == 'write'
46 46
47 47 assert pod.perm_origin_stack == {
48 48 'thing': [('read', 'default', 1), ('write', 'admin', 1)]}
49 49
50 50 pod['other'] = 'write', 'default', 8
51 51
52 52 assert pod.perm_origin_stack == {
53 53 'other': [('write', 'default', 8)],
54 54 'thing': [('read', 'default', 1), ('write', 'admin', 1)]}
55 55
56 56 pod['other'] = 'none', 'override', 8
57 57
58 58 assert pod.perm_origin_stack == {
59 59 'other': [('write', 'default', 8), ('none', 'override', 8)],
60 60 'thing': [('read', 'default', 1), ('write', 'admin', 1)]}
61 61
62 62 with pytest.raises(ValueError):
63 63 pod['thing'] = 'read'
64 64
65 65
66 66 def test_cached_perms_data(user_regular, backend_random):
67 67 permissions = get_permissions(user_regular)
68 68 repo_name = backend_random.repo.repo_name
69 69 expected_global_permissions = {
70 70 'repository.read', 'group.read', 'usergroup.read'}
71 71 assert expected_global_permissions.issubset(permissions['global'])
72 72 assert permissions['repositories'][repo_name] == 'repository.read'
73 73
74 74
75 75 def test_cached_perms_data_with_admin_user(user_regular, backend_random):
76 76 permissions = get_permissions(user_regular, user_is_admin=True)
77 77 repo_name = backend_random.repo.repo_name
78 78 assert 'hg.admin' in permissions['global']
79 79 assert permissions['repositories'][repo_name] == 'repository.admin'
80 80
81 81
82 82 def test_cached_perms_data_with_admin_user_extended_calculation(user_regular, backend_random):
83 83 permissions = get_permissions(user_regular, user_is_admin=True,
84 84 calculate_super_admin=True)
85 85 repo_name = backend_random.repo.repo_name
86 86 assert 'hg.admin' in permissions['global']
87 87 assert permissions['repositories'][repo_name] == 'repository.admin'
88 88
89 89
90 90 def test_cached_perms_data_user_group_global_permissions(user_util):
91 91 user, user_group = user_util.create_user_with_group()
92 92 user_group.inherit_default_permissions = False
93 93
94 94 granted_permission = 'repository.write'
95 95 UserGroupModel().grant_perm(user_group, granted_permission)
96 96 Session().commit()
97 97
98 98 permissions = get_permissions(user)
99 99 assert granted_permission in permissions['global']
100 100
101 101
102 102 @pytest.mark.xfail(reason="Not implemented, see TODO note")
103 103 def test_cached_perms_data_user_group_global_permissions_(user_util):
104 104 user, user_group = user_util.create_user_with_group()
105 105
106 106 granted_permission = 'repository.write'
107 107 UserGroupModel().grant_perm(user_group, granted_permission)
108 108 Session().commit()
109 109
110 110 permissions = get_permissions(user)
111 111 assert granted_permission in permissions['global']
112 112
113 113
114 114 def test_cached_perms_data_user_global_permissions(user_util):
115 115 user = user_util.create_user()
116 116 UserModel().grant_perm(user, 'repository.none')
117 117 Session().commit()
118 118
119 119 permissions = get_permissions(user, user_inherit_default_permissions=True)
120 120 assert 'repository.read' in permissions['global']
121 121
122 122
123 123 def test_cached_perms_data_repository_permissions_on_private_repository(
124 124 backend_random, user_util):
125 125 user, user_group = user_util.create_user_with_group()
126 126
127 127 repo = backend_random.create_repo()
128 128 repo.private = True
129 129
130 130 granted_permission = 'repository.write'
131 131 RepoModel().grant_user_group_permission(
132 132 repo, user_group.users_group_name, granted_permission)
133 133 Session().commit()
134 134
135 135 permissions = get_permissions(user)
136 136 assert permissions['repositories'][repo.repo_name] == granted_permission
137 137
138 138
139 139 def test_cached_perms_data_repository_permissions_for_owner(
140 140 backend_random, user_util):
141 141 user = user_util.create_user()
142 142
143 143 repo = backend_random.create_repo()
144 144 repo.user_id = user.user_id
145 145
146 146 permissions = get_permissions(user)
147 147 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
148 148
149 149 # TODO: johbo: Make cleanup in UserUtility smarter, then remove this hack
150 repo.user_id = User.get_default_user().user_id
150 repo.user_id = User.get_default_user_id()
151 151
152 152
153 153 def test_cached_perms_data_repository_permissions_not_inheriting_defaults(
154 154 backend_random, user_util):
155 155 user = user_util.create_user()
156 156 repo = backend_random.create_repo()
157 157
158 158 # Don't inherit default object permissions
159 159 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
160 160 Session().commit()
161 161
162 162 permissions = get_permissions(user)
163 163 assert permissions['repositories'][repo.repo_name] == 'repository.none'
164 164
165 165
166 166 def test_cached_perms_data_default_permissions_on_repository_group(user_util):
167 167 # Have a repository group with default permissions set
168 168 repo_group = user_util.create_repo_group()
169 169 default_user = User.get_default_user()
170 170 user_util.grant_user_permission_to_repo_group(
171 171 repo_group, default_user, 'repository.write')
172 172 user = user_util.create_user()
173 173
174 174 permissions = get_permissions(user)
175 175 assert permissions['repositories_groups'][repo_group.group_name] == \
176 176 'repository.write'
177 177
178 178
179 179 def test_cached_perms_data_default_permissions_on_repository_group_owner(
180 180 user_util):
181 181 # Have a repository group
182 182 repo_group = user_util.create_repo_group()
183 183 default_user = User.get_default_user()
184 184
185 185 # Add a permission for the default user to hit the code path
186 186 user_util.grant_user_permission_to_repo_group(
187 187 repo_group, default_user, 'repository.write')
188 188
189 189 # Have an owner of the group
190 190 user = user_util.create_user()
191 191 repo_group.user_id = user.user_id
192 192
193 193 permissions = get_permissions(user)
194 194 assert permissions['repositories_groups'][repo_group.group_name] == \
195 195 'group.admin'
196 196
197 197
198 198 def test_cached_perms_data_default_permissions_on_repository_group_no_inherit(
199 199 user_util):
200 200 # Have a repository group
201 201 repo_group = user_util.create_repo_group()
202 202 default_user = User.get_default_user()
203 203
204 204 # Add a permission for the default user to hit the code path
205 205 user_util.grant_user_permission_to_repo_group(
206 206 repo_group, default_user, 'repository.write')
207 207
208 208 # Don't inherit default object permissions
209 209 user = user_util.create_user()
210 210 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
211 211 Session().commit()
212 212
213 213 permissions = get_permissions(user)
214 214 assert permissions['repositories_groups'][repo_group.group_name] == \
215 215 'group.none'
216 216
217 217
218 218 def test_cached_perms_data_repository_permissions_from_user_group(
219 219 user_util, backend_random):
220 220 user, user_group = user_util.create_user_with_group()
221 221
222 222 # Needs a second user group to make sure that we select the right
223 223 # permissions.
224 224 user_group2 = user_util.create_user_group()
225 225 UserGroupModel().add_user_to_group(user_group2, user)
226 226
227 227 repo = backend_random.create_repo()
228 228
229 229 RepoModel().grant_user_group_permission(
230 230 repo, user_group.users_group_name, 'repository.read')
231 231 RepoModel().grant_user_group_permission(
232 232 repo, user_group2.users_group_name, 'repository.write')
233 233 Session().commit()
234 234
235 235 permissions = get_permissions(user)
236 236 assert permissions['repositories'][repo.repo_name] == 'repository.write'
237 237
238 238
239 239 def test_cached_perms_data_repository_permissions_from_user_group_owner(
240 240 user_util, backend_random):
241 241 user, user_group = user_util.create_user_with_group()
242 242
243 243 repo = backend_random.create_repo()
244 244 repo.user_id = user.user_id
245 245
246 246 RepoModel().grant_user_group_permission(
247 247 repo, user_group.users_group_name, 'repository.write')
248 248 Session().commit()
249 249
250 250 permissions = get_permissions(user)
251 251 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
252 252
253 253
254 254 def test_cached_perms_data_user_repository_permissions(
255 255 user_util, backend_random):
256 256 user = user_util.create_user()
257 257 repo = backend_random.create_repo()
258 258 granted_permission = 'repository.write'
259 259 RepoModel().grant_user_permission(repo, user, granted_permission)
260 260 Session().commit()
261 261
262 262 permissions = get_permissions(user)
263 263 assert permissions['repositories'][repo.repo_name] == granted_permission
264 264
265 265
266 266 def test_cached_perms_data_user_repository_permissions_explicit(
267 267 user_util, backend_random):
268 268 user = user_util.create_user()
269 269 repo = backend_random.create_repo()
270 270 granted_permission = 'repository.none'
271 271 RepoModel().grant_user_permission(repo, user, granted_permission)
272 272 Session().commit()
273 273
274 274 permissions = get_permissions(user, explicit=True)
275 275 assert permissions['repositories'][repo.repo_name] == granted_permission
276 276
277 277
278 278 def test_cached_perms_data_user_repository_permissions_owner(
279 279 user_util, backend_random):
280 280 user = user_util.create_user()
281 281 repo = backend_random.create_repo()
282 282 repo.user_id = user.user_id
283 283 RepoModel().grant_user_permission(repo, user, 'repository.write')
284 284 Session().commit()
285 285
286 286 permissions = get_permissions(user)
287 287 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
288 288
289 289
290 290 def test_cached_perms_data_repository_groups_permissions_inherited(
291 291 user_util, backend_random):
292 292 user, user_group = user_util.create_user_with_group()
293 293
294 294 # Needs a second group to hit the last condition
295 295 user_group2 = user_util.create_user_group()
296 296 UserGroupModel().add_user_to_group(user_group2, user)
297 297
298 298 repo_group = user_util.create_repo_group()
299 299
300 300 user_util.grant_user_group_permission_to_repo_group(
301 301 repo_group, user_group, 'group.read')
302 302 user_util.grant_user_group_permission_to_repo_group(
303 303 repo_group, user_group2, 'group.write')
304 304
305 305 permissions = get_permissions(user)
306 306 assert permissions['repositories_groups'][repo_group.group_name] == \
307 307 'group.write'
308 308
309 309
310 310 def test_cached_perms_data_repository_groups_permissions_inherited_owner(
311 311 user_util, backend_random):
312 312 user, user_group = user_util.create_user_with_group()
313 313 repo_group = user_util.create_repo_group()
314 314 repo_group.user_id = user.user_id
315 315
316 316 granted_permission = 'group.write'
317 317 user_util.grant_user_group_permission_to_repo_group(
318 318 repo_group, user_group, granted_permission)
319 319
320 320 permissions = get_permissions(user)
321 321 assert permissions['repositories_groups'][repo_group.group_name] == \
322 322 'group.admin'
323 323
324 324
325 325 def test_cached_perms_data_repository_groups_permissions(
326 326 user_util, backend_random):
327 327 user = user_util.create_user()
328 328
329 329 repo_group = user_util.create_repo_group()
330 330
331 331 granted_permission = 'group.write'
332 332 user_util.grant_user_permission_to_repo_group(
333 333 repo_group, user, granted_permission)
334 334
335 335 permissions = get_permissions(user)
336 336 assert permissions['repositories_groups'][repo_group.group_name] == \
337 337 'group.write'
338 338
339 339
340 340 def test_cached_perms_data_repository_groups_permissions_explicit(
341 341 user_util, backend_random):
342 342 user = user_util.create_user()
343 343
344 344 repo_group = user_util.create_repo_group()
345 345
346 346 granted_permission = 'group.none'
347 347 user_util.grant_user_permission_to_repo_group(
348 348 repo_group, user, granted_permission)
349 349
350 350 permissions = get_permissions(user, explicit=True)
351 351 assert permissions['repositories_groups'][repo_group.group_name] == \
352 352 'group.none'
353 353
354 354
355 355 def test_cached_perms_data_repository_groups_permissions_owner(
356 356 user_util, backend_random):
357 357 user = user_util.create_user()
358 358
359 359 repo_group = user_util.create_repo_group()
360 360 repo_group.user_id = user.user_id
361 361
362 362 granted_permission = 'group.write'
363 363 user_util.grant_user_permission_to_repo_group(
364 364 repo_group, user, granted_permission)
365 365
366 366 permissions = get_permissions(user)
367 367 assert permissions['repositories_groups'][repo_group.group_name] == \
368 368 'group.admin'
369 369
370 370
371 371 def test_cached_perms_data_user_group_permissions_inherited(
372 372 user_util, backend_random):
373 373 user, user_group = user_util.create_user_with_group()
374 374 user_group2 = user_util.create_user_group()
375 375 UserGroupModel().add_user_to_group(user_group2, user)
376 376
377 377 target_user_group = user_util.create_user_group()
378 378
379 379 user_util.grant_user_group_permission_to_user_group(
380 380 target_user_group, user_group, 'usergroup.read')
381 381 user_util.grant_user_group_permission_to_user_group(
382 382 target_user_group, user_group2, 'usergroup.write')
383 383
384 384 permissions = get_permissions(user)
385 385 assert permissions['user_groups'][target_user_group.users_group_name] == \
386 386 'usergroup.write'
387 387
388 388
389 389 def test_cached_perms_data_user_group_permissions(
390 390 user_util, backend_random):
391 391 user = user_util.create_user()
392 392 user_group = user_util.create_user_group()
393 393 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.write')
394 394 Session().commit()
395 395
396 396 permissions = get_permissions(user)
397 397 assert permissions['user_groups'][user_group.users_group_name] == \
398 398 'usergroup.write'
399 399
400 400
401 401 def test_cached_perms_data_user_group_permissions_explicit(
402 402 user_util, backend_random):
403 403 user = user_util.create_user()
404 404 user_group = user_util.create_user_group()
405 405 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.none')
406 406 Session().commit()
407 407
408 408 permissions = get_permissions(user, explicit=True)
409 409 assert permissions['user_groups'][user_group.users_group_name] == \
410 410 'usergroup.none'
411 411
412 412
413 413 def test_cached_perms_data_user_group_permissions_not_inheriting_defaults(
414 414 user_util, backend_random):
415 415 user = user_util.create_user()
416 416 user_group = user_util.create_user_group()
417 417
418 418 # Don't inherit default object permissions
419 419 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
420 420 Session().commit()
421 421
422 422 permissions = get_permissions(user)
423 423 assert permissions['user_groups'][user_group.users_group_name] == \
424 424 'usergroup.none'
425 425
426 426
427 427 def test_permission_calculator_admin_permissions(
428 428 user_util, backend_random):
429 429 user = user_util.create_user()
430 430 user_group = user_util.create_user_group()
431 431 repo = backend_random.repo
432 432 repo_group = user_util.create_repo_group()
433 433
434 434 calculator = auth.PermissionCalculator(
435 435 user.user_id, {}, False, False, True, 'higherwin')
436 436 permissions = calculator._calculate_admin_permissions()
437 437
438 438 assert permissions['repositories_groups'][repo_group.group_name] == \
439 439 'group.admin'
440 440 assert permissions['user_groups'][user_group.users_group_name] == \
441 441 'usergroup.admin'
442 442 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
443 443 assert 'hg.admin' in permissions['global']
444 444
445 445
446 446 def test_permission_calculator_repository_permissions_robustness_from_group(
447 447 user_util, backend_random):
448 448 user, user_group = user_util.create_user_with_group()
449 449
450 450 RepoModel().grant_user_group_permission(
451 451 backend_random.repo, user_group.users_group_name, 'repository.write')
452 452
453 453 calculator = auth.PermissionCalculator(
454 454 user.user_id, {}, False, False, False, 'higherwin')
455 455 calculator._calculate_repository_permissions()
456 456
457 457
458 458 def test_permission_calculator_repository_permissions_robustness_from_user(
459 459 user_util, backend_random):
460 460 user = user_util.create_user()
461 461
462 462 RepoModel().grant_user_permission(
463 463 backend_random.repo, user, 'repository.write')
464 464 Session().commit()
465 465
466 466 calculator = auth.PermissionCalculator(
467 467 user.user_id, {}, False, False, False, 'higherwin')
468 468 calculator._calculate_repository_permissions()
469 469
470 470
471 471 def test_permission_calculator_repo_group_permissions_robustness_from_group(
472 472 user_util, backend_random):
473 473 user, user_group = user_util.create_user_with_group()
474 474 repo_group = user_util.create_repo_group()
475 475
476 476 user_util.grant_user_group_permission_to_repo_group(
477 477 repo_group, user_group, 'group.write')
478 478
479 479 calculator = auth.PermissionCalculator(
480 480 user.user_id, {}, False, False, False, 'higherwin')
481 481 calculator._calculate_repository_group_permissions()
482 482
483 483
484 484 def test_permission_calculator_repo_group_permissions_robustness_from_user(
485 485 user_util, backend_random):
486 486 user = user_util.create_user()
487 487 repo_group = user_util.create_repo_group()
488 488
489 489 user_util.grant_user_permission_to_repo_group(
490 490 repo_group, user, 'group.write')
491 491
492 492 calculator = auth.PermissionCalculator(
493 493 user.user_id, {}, False, False, False, 'higherwin')
494 494 calculator._calculate_repository_group_permissions()
495 495
496 496
497 497 def test_permission_calculator_user_group_permissions_robustness_from_group(
498 498 user_util, backend_random):
499 499 user, user_group = user_util.create_user_with_group()
500 500 target_user_group = user_util.create_user_group()
501 501
502 502 user_util.grant_user_group_permission_to_user_group(
503 503 target_user_group, user_group, 'usergroup.write')
504 504
505 505 calculator = auth.PermissionCalculator(
506 506 user.user_id, {}, False, False, False, 'higherwin')
507 507 calculator._calculate_user_group_permissions()
508 508
509 509
510 510 def test_permission_calculator_user_group_permissions_robustness_from_user(
511 511 user_util, backend_random):
512 512 user = user_util.create_user()
513 513 target_user_group = user_util.create_user_group()
514 514
515 515 user_util.grant_user_permission_to_user_group(
516 516 target_user_group, user, 'usergroup.write')
517 517
518 518 calculator = auth.PermissionCalculator(
519 519 user.user_id, {}, False, False, False, 'higherwin')
520 520 calculator._calculate_user_group_permissions()
521 521
522 522
523 523 @pytest.mark.parametrize("algo, new_permission, old_permission, expected", [
524 524 ('higherwin', 'repository.none', 'repository.none', 'repository.none'),
525 525 ('higherwin', 'repository.read', 'repository.none', 'repository.read'),
526 526 ('lowerwin', 'repository.write', 'repository.write', 'repository.write'),
527 527 ('lowerwin', 'repository.read', 'repository.write', 'repository.read'),
528 528 ])
529 529 def test_permission_calculator_choose_permission(
530 530 user_regular, algo, new_permission, old_permission, expected):
531 531 calculator = auth.PermissionCalculator(
532 532 user_regular.user_id, {}, False, False, False, algo)
533 533 result = calculator._choose_permission(new_permission, old_permission)
534 534 assert result == expected
535 535
536 536
537 537 def test_permission_calculator_choose_permission_raises_on_wrong_algo(
538 538 user_regular):
539 539 calculator = auth.PermissionCalculator(
540 540 user_regular.user_id, {}, False, False, False, 'invalid')
541 541 result = calculator._choose_permission(
542 542 'repository.read', 'repository.read')
543 543 # TODO: johbo: This documents the existing behavior. Think of an
544 544 # improvement.
545 545 assert result is None
546 546
547 547
548 548 def test_auth_user_get_cookie_store_for_normal_user(user_util):
549 549 user = user_util.create_user()
550 550 auth_user = auth.AuthUser(user_id=user.user_id)
551 551 expected_data = {
552 552 'username': user.username,
553 553 'user_id': user.user_id,
554 554 'password': md5(user.password),
555 555 'is_authenticated': False
556 556 }
557 557 assert auth_user.get_cookie_store() == expected_data
558 558
559 559
560 560 def test_auth_user_get_cookie_store_for_default_user():
561 561 default_user = User.get_default_user()
562 562 auth_user = auth.AuthUser()
563 563 expected_data = {
564 564 'username': User.DEFAULT_USER,
565 565 'user_id': default_user.user_id,
566 566 'password': md5(default_user.password),
567 567 'is_authenticated': True
568 568 }
569 569 assert auth_user.get_cookie_store() == expected_data
570 570
571 571
572 572 def get_permissions(user, **kwargs):
573 573 """
574 574 Utility filling in useful defaults into the call to `_cached_perms_data`.
575 575
576 576 Fill in `**kwargs` if specific values are needed for a test.
577 577 """
578 578 call_args = {
579 579 'user_id': user.user_id,
580 580 'scope': {},
581 581 'user_is_admin': False,
582 582 'user_inherit_default_permissions': False,
583 583 'explicit': False,
584 584 'algo': 'higherwin',
585 585 'calculate_super_admin': False,
586 586 }
587 587 call_args.update(kwargs)
588 588 permissions = auth._cached_perms_data(**call_args)
589 589 return permissions
590 590
591 591
592 592 class TestGenerateAuthToken(object):
593 593 def test_salt_is_used_when_specified(self):
594 594 salt = 'abcde'
595 595 user_name = 'test_user'
596 596 result = auth.generate_auth_token(user_name, salt)
597 597 expected_result = sha1(user_name + salt).hexdigest()
598 598 assert result == expected_result
599 599
600 600 def test_salt_is_geneated_when_not_specified(self):
601 601 user_name = 'test_user'
602 602 random_salt = os.urandom(16)
603 603 with patch.object(auth, 'os') as os_mock:
604 604 os_mock.urandom.return_value = random_salt
605 605 result = auth.generate_auth_token(user_name)
606 606 expected_result = sha1(user_name + random_salt).hexdigest()
607 607 assert result == expected_result
608 608
609 609
610 610 @pytest.mark.parametrize("test_token, test_roles, auth_result, expected_tokens", [
611 611 ('', None, False,
612 612 []),
613 613 ('wrongtoken', None, False,
614 614 []),
615 615 ('abracadabra_vcs', [AuthTokenModel.cls.ROLE_API], False,
616 616 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
617 617 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
618 618 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
619 619 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
620 620 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1),
621 621 ('abracadabra_http', AuthTokenModel.cls.ROLE_HTTP, -1)]),
622 622 ])
623 623 def test_auth_by_token(test_token, test_roles, auth_result, expected_tokens,
624 624 user_util):
625 625 user = user_util.create_user()
626 626 user_id = user.user_id
627 627 for token, role, expires in expected_tokens:
628 628 new_token = AuthTokenModel().create(user_id, u'test-token', expires, role)
629 629 new_token.api_key = token # inject known name for testing...
630 630
631 631 assert auth_result == user.authenticate_by_token(
632 632 test_token, roles=test_roles)
@@ -1,325 +1,325 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 functools
22 22
23 23 import pytest
24 24
25 25 from rhodecode.model.db import RepoGroup, User
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.repo_group import RepoGroupModel
28 28 from rhodecode.tests.models.common import (
29 29 _create_project_tree, check_tree_perms, _get_perms, _check_expected_count,
30 30 expected_count, _destroy_project_tree)
31 31
32 32
33 33 test_u1_id = None
34 34 _get_repo_perms = None
35 35 _get_group_perms = None
36 36
37 37
38 38 @pytest.fixture(autouse=True)
39 39 def setup_read_permission():
40 40 permissions_setup_func()
41 41
42 42
43 43 def permissions_setup_func(group_name='g0', perm='group.read', recursive='all',
44 44 user_id=None):
45 45 """
46 46 Resets all permissions to perm attribute
47 47 """
48 48 if not user_id:
49 49 user_id = test_u1_id
50 50 # called by the @with_setup decorator also reset the default user stuff
51 51 permissions_setup_func(group_name, perm, recursive,
52 user_id=User.get_default_user().user_id)
52 user_id=User.get_default_user_id())
53 53
54 54 # TODO: DRY, compare test_user_group:permissions_setup_func
55 55 repo_group = RepoGroup.get_by_group_name(group_name=group_name)
56 56 if not repo_group:
57 57 raise Exception('Cannot get group %s' % group_name)
58 58
59 59 perm_updates = [[user_id, perm, 'user']]
60 60 RepoGroupModel().update_permissions(repo_group,
61 61 perm_updates=perm_updates,
62 62 recursive=recursive, check_perms=False)
63 63 Session().commit()
64 64
65 65
66 66 @pytest.fixture(scope='module', autouse=True)
67 67 def prepare(request, baseapp):
68 68 global test_u1_id, _get_repo_perms, _get_group_perms
69 69 test_u1 = _create_project_tree()
70 70 Session().commit()
71 71 test_u1_id = test_u1.user_id
72 72 _get_repo_perms = functools.partial(_get_perms, key='repositories',
73 73 test_u1_id=test_u1_id)
74 74 _get_group_perms = functools.partial(_get_perms, key='repositories_groups',
75 75 test_u1_id=test_u1_id)
76 76
77 77 @request.addfinalizer
78 78 def cleanup():
79 79 _destroy_project_tree(test_u1_id)
80 80
81 81
82 82 def test_user_permissions_on_group_without_recursive_mode():
83 83 # set permission to g0 non-recursive mode
84 84 recursive = 'none'
85 85 group = 'g0'
86 86 permissions_setup_func(group, 'group.write', recursive=recursive)
87 87
88 88 items = [x for x in _get_repo_perms(group, recursive)]
89 89 expected = 0
90 90 assert len(items) == expected, ' %s != %s' % (len(items), expected)
91 91 for name, perm in items:
92 92 check_tree_perms(name, perm, group, 'repository.read')
93 93
94 94 items = [x for x in _get_group_perms(group, recursive)]
95 95 expected = 1
96 96 assert len(items) == expected, ' %s != %s' % (len(items), expected)
97 97 for name, perm in items:
98 98 check_tree_perms(name, perm, group, 'group.write')
99 99
100 100
101 101 def test_user_permissions_on_group_without_recursive_mode_subgroup():
102 102 # set permission to g0 non-recursive mode
103 103 recursive = 'none'
104 104 group = 'g0/g0_1'
105 105 permissions_setup_func(group, 'group.write', recursive=recursive)
106 106
107 107 items = [x for x in _get_repo_perms(group, recursive)]
108 108 expected = 0
109 109 assert len(items) == expected, ' %s != %s' % (len(items), expected)
110 110 for name, perm in items:
111 111 check_tree_perms(name, perm, group, 'repository.read')
112 112
113 113 items = [x for x in _get_group_perms(group, recursive)]
114 114 expected = 1
115 115 assert len(items) == expected, ' %s != %s' % (len(items), expected)
116 116 for name, perm in items:
117 117 check_tree_perms(name, perm, group, 'group.write')
118 118
119 119
120 120 def test_user_permissions_on_group_with_recursive_mode():
121 121
122 122 # set permission to g0 recursive mode, all children including
123 123 # other repos and groups should have this permission now set !
124 124 recursive = 'all'
125 125 group = 'g0'
126 126 permissions_setup_func(group, 'group.write', recursive=recursive)
127 127
128 128 repo_items = [x for x in _get_repo_perms(group, recursive)]
129 129 items = [x for x in _get_group_perms(group, recursive)]
130 130 _check_expected_count(items, repo_items, expected_count(group, True))
131 131
132 132 for name, perm in repo_items:
133 133 check_tree_perms(name, perm, group, 'repository.write')
134 134
135 135 for name, perm in items:
136 136 check_tree_perms(name, perm, group, 'group.write')
137 137
138 138
139 139 def test_user_permissions_on_group_with_recursive_mode_for_default_user():
140 140
141 141 # set permission to g0 recursive mode, all children including
142 142 # other repos and groups should have this permission now set !
143 143 recursive = 'all'
144 144 group = 'g0'
145 default_user_id = User.get_default_user().user_id
145 default_user_id = User.get_default_user_id()
146 146 permissions_setup_func(group, 'group.write', recursive=recursive,
147 147 user_id=default_user_id)
148 148
149 149 # change default to get perms for default user
150 150 _get_repo_perms = functools.partial(_get_perms, key='repositories',
151 151 test_u1_id=default_user_id)
152 152 _get_group_perms = functools.partial(_get_perms, key='repositories_groups',
153 153 test_u1_id=default_user_id)
154 154
155 155 repo_items = [x for x in _get_repo_perms(group, recursive)]
156 156 items = [x for x in _get_group_perms(group, recursive)]
157 157 _check_expected_count(items, repo_items, expected_count(group, True))
158 158
159 159 for name, perm in repo_items:
160 160 check_tree_perms(name, perm, group, 'repository.write')
161 161
162 162 for name, perm in items:
163 163 check_tree_perms(name, perm, group, 'group.write')
164 164
165 165
166 166 def test_user_permissions_on_group_with_recursive_mode_inner_group():
167 167 # set permission to g0_3 group to none
168 168 recursive = 'all'
169 169 group = 'g0/g0_3'
170 170 permissions_setup_func(group, 'group.none', recursive=recursive)
171 171
172 172 repo_items = [x for x in _get_repo_perms(group, recursive)]
173 173 items = [x for x in _get_group_perms(group, recursive)]
174 174 _check_expected_count(items, repo_items, expected_count(group, True))
175 175
176 176 for name, perm in repo_items:
177 177 check_tree_perms(name, perm, group, 'repository.none')
178 178
179 179 for name, perm in items:
180 180 check_tree_perms(name, perm, group, 'group.none')
181 181
182 182
183 183 def test_user_permissions_on_group_with_recursive_mode_deepest():
184 184 # set permission to g0_3 group to none
185 185 recursive = 'all'
186 186 group = 'g0/g0_1/g0_1_1'
187 187 permissions_setup_func(group, 'group.write', recursive=recursive)
188 188
189 189 repo_items = [x for x in _get_repo_perms(group, recursive)]
190 190 items = [x for x in _get_group_perms(group, recursive)]
191 191 _check_expected_count(items, repo_items, expected_count(group, True))
192 192
193 193 for name, perm in repo_items:
194 194 check_tree_perms(name, perm, group, 'repository.write')
195 195
196 196 for name, perm in items:
197 197 check_tree_perms(name, perm, group, 'group.write')
198 198
199 199
200 200 def test_user_permissions_on_group_with_recursive_mode_only_with_repos():
201 201 # set permission to g0_3 group to none
202 202 recursive = 'all'
203 203 group = 'g0/g0_2'
204 204 permissions_setup_func(group, 'group.admin', recursive=recursive)
205 205
206 206 repo_items = [x for x in _get_repo_perms(group, recursive)]
207 207 items = [x for x in _get_group_perms(group, recursive)]
208 208 _check_expected_count(items, repo_items, expected_count(group, True))
209 209
210 210 for name, perm in repo_items:
211 211 check_tree_perms(name, perm, group, 'repository.admin')
212 212
213 213 for name, perm in items:
214 214 check_tree_perms(name, perm, group, 'group.admin')
215 215
216 216
217 217 def test_user_permissions_on_group_with_recursive_repo_mode_for_default_user():
218 218 # set permission to g0/g0_1 recursive repos only mode, all children
219 219 # including other repos should have this permission now set, inner groups
220 220 # are excluded!
221 221 recursive = 'repos'
222 222 group = 'g0/g0_1'
223 223 perm = 'group.none'
224 default_user_id = User.get_default_user().user_id
224 default_user_id = User.get_default_user_id()
225 225
226 226 # TODO: workaround due to different setup calls, adept to py.test style
227 227 permissions_setup_func()
228 228 permissions_setup_func(group, perm, recursive=recursive,
229 229 user_id=default_user_id)
230 230
231 231 # change default to get perms for default user
232 232 _get_repo_perms = functools.partial(_get_perms, key='repositories',
233 233 test_u1_id=default_user_id)
234 234 _get_group_perms = functools.partial(_get_perms, key='repositories_groups',
235 235 test_u1_id=default_user_id)
236 236
237 237 repo_items = [x for x in _get_repo_perms(group, recursive)]
238 238 items = [x for x in _get_group_perms(group, recursive)]
239 239 _check_expected_count(items, repo_items, expected_count(group, True))
240 240
241 241 for name, perm in repo_items:
242 242 check_tree_perms(name, perm, group, 'repository.none')
243 243
244 244 for name, perm in items:
245 245 # permission is set with repos only mode, but we also change the
246 246 # permission on the group we trigger the apply to children from, thus
247 247 # we need to change its permission check
248 248 old_perm = 'group.read'
249 249 if name == group:
250 250 old_perm = perm
251 251 check_tree_perms(name, perm, group, old_perm)
252 252
253 253
254 254 def test_user_permissions_on_group_with_recursive_repo_mode_inner_group():
255 255 # set permission to g0_3 group to none, with recursive repos only
256 256 recursive = 'repos'
257 257 group = 'g0/g0_3'
258 258 perm = 'group.none'
259 259 permissions_setup_func(group, perm, recursive=recursive)
260 260
261 261 repo_items = [x for x in _get_repo_perms(group, recursive)]
262 262 items = [x for x in _get_group_perms(group, recursive)]
263 263 _check_expected_count(items, repo_items, expected_count(group, True))
264 264
265 265 for name, perm in repo_items:
266 266 check_tree_perms(name, perm, group, 'repository.none')
267 267
268 268 for name, perm in items:
269 269 # permission is set with repos only mode, but we also change the
270 270 # permission on the group we trigger the apply to children from, thus
271 271 # we need to change its permission check
272 272 old_perm = 'group.read'
273 273 if name == group:
274 274 old_perm = perm
275 275 check_tree_perms(name, perm, group, old_perm)
276 276
277 277
278 278 def test_user_permissions_on_group_with_rec_group_mode_for_default_user():
279 279 # set permission to g0/g0_1 with recursive groups only mode, all children
280 280 # sincluding other groups should have this permission now set. repositories
281 281 # should remain intact as we use groups only mode !
282 282 recursive = 'groups'
283 283 group = 'g0/g0_1'
284 default_user_id = User.get_default_user().user_id
284 default_user_id = User.get_default_user_id()
285 285
286 286 # TODO: workaround due to different setup calls, adept to py.test style
287 287 permissions_setup_func()
288 288 permissions_setup_func(group, 'group.write', recursive=recursive,
289 289 user_id=default_user_id)
290 290
291 291 # change default to get perms for default user
292 292 _get_repo_perms = functools.partial(_get_perms, key='repositories',
293 293 test_u1_id=default_user_id)
294 294 _get_group_perms = functools.partial(_get_perms, key='repositories_groups',
295 295 test_u1_id=default_user_id)
296 296
297 297 repo_items = [x for x in _get_repo_perms(group, recursive)]
298 298 items = [x for x in _get_group_perms(group, recursive)]
299 299 _check_expected_count(items, repo_items, expected_count(group, True))
300 300
301 301 for name, perm in repo_items:
302 302 check_tree_perms(name, perm, group, 'repository.read')
303 303
304 304 for name, perm in items:
305 305 check_tree_perms(name, perm, group, 'group.write')
306 306
307 307
308 308 def test_user_permissions_on_group_with_recursive_group_mode_inner_group():
309 309 # set permission to g0_3 group to none, with recursive mode for groups only
310 310 recursive = 'groups'
311 311 group = 'g0/g0_3'
312 312
313 313 # TODO: workaround due to different setup calls, adept to py.test style
314 314 permissions_setup_func()
315 315 permissions_setup_func(group, 'group.none', recursive=recursive)
316 316
317 317 repo_items = [x for x in _get_repo_perms(group, recursive)]
318 318 items = [x for x in _get_group_perms(group, recursive)]
319 319 _check_expected_count(items, repo_items, expected_count(group, True))
320 320
321 321 for name, perm in repo_items:
322 322 check_tree_perms(name, perm, group, 'repository.read')
323 323
324 324 for name, perm in items:
325 325 check_tree_perms(name, perm, group, 'group.none')
General Comments 0
You need to be logged in to leave comments. Login now