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