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