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