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