##// END OF EJS Templates
comments: added rcextensions hoooks for comment editing, and renamed methods to remove odd log_ prefix which...
marcink -
r4445:5a5c90c0 default
parent child Browse files
Show More
@@ -1,60 +1,64 b''
1 1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 rcextensions module, please edit `hooks.py` to over write hooks logic
21 21 """
22 22
23 23 from .hooks import (
24 24 _create_repo_hook,
25 25 _create_repo_group_hook,
26 26 _pre_create_user_hook,
27 27 _create_user_hook,
28 28 _comment_commit_repo_hook,
29 _comment_edit_commit_repo_hook,
29 30 _delete_repo_hook,
30 31 _delete_user_hook,
31 32 _pre_push_hook,
32 33 _push_hook,
33 34 _pre_pull_hook,
34 35 _pull_hook,
35 36 _create_pull_request_hook,
36 37 _review_pull_request_hook,
37 38 _comment_pull_request_hook,
39 _comment_edit_pull_request_hook,
38 40 _update_pull_request_hook,
39 41 _merge_pull_request_hook,
40 42 _close_pull_request_hook,
41 43 )
42 44
43 45 # set as module attributes, we use those to call hooks. *do not change this*
44 46 CREATE_REPO_HOOK = _create_repo_hook
45 47 COMMENT_COMMIT_REPO_HOOK = _comment_commit_repo_hook
48 COMMENT_EDIT_COMMIT_REPO_HOOK = _comment_edit_commit_repo_hook
46 49 CREATE_REPO_GROUP_HOOK = _create_repo_group_hook
47 50 PRE_CREATE_USER_HOOK = _pre_create_user_hook
48 51 CREATE_USER_HOOK = _create_user_hook
49 52 DELETE_REPO_HOOK = _delete_repo_hook
50 53 DELETE_USER_HOOK = _delete_user_hook
51 54 PRE_PUSH_HOOK = _pre_push_hook
52 55 PUSH_HOOK = _push_hook
53 56 PRE_PULL_HOOK = _pre_pull_hook
54 57 PULL_HOOK = _pull_hook
55 58 CREATE_PULL_REQUEST = _create_pull_request_hook
56 59 REVIEW_PULL_REQUEST = _review_pull_request_hook
57 60 COMMENT_PULL_REQUEST = _comment_pull_request_hook
61 COMMENT_EDIT_PULL_REQUEST = _comment_edit_pull_request_hook
58 62 UPDATE_PULL_REQUEST = _update_pull_request_hook
59 63 MERGE_PULL_REQUEST = _merge_pull_request_hook
60 64 CLOSE_PULL_REQUEST = _close_pull_request_hook
@@ -1,492 +1,551 b''
1 1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 from .utils import DotDict, HookResponse, has_kwargs
21 21
22 22 log = logging.getLogger('rhodecode.' + __name__)
23 23
24 24 # Config shortcut to keep, all configuration in one place
25 25 # Example: api_key = CONFIG.my_config.api_key
26 26 CONFIG = DotDict(
27 27 my_config=DotDict(
28 28 api_key='<secret>',
29 29 ),
30 30
31 31 )
32 32
33 33
34 34 @has_kwargs({
35 35 'repo_name': '',
36 36 'repo_type': '',
37 37 'description': '',
38 38 'private': '',
39 39 'created_on': '',
40 40 'enable_downloads': '',
41 41 'repo_id': '',
42 42 'user_id': '',
43 43 'enable_statistics': '',
44 44 'clone_uri': '',
45 45 'fork_id': '',
46 46 'group_id': '',
47 47 'created_by': ''
48 48 })
49 49 def _create_repo_hook(*args, **kwargs):
50 50 """
51 51 POST CREATE REPOSITORY HOOK. This function will be executed after
52 52 each repository is created. kwargs available:
53 53
54 54 """
55 55 return HookResponse(0, '')
56 56
57 57
58 58 @has_kwargs({
59 59 'repo_name': '',
60 60 'repo_type': '',
61 61 'description': '',
62 62 'private': '',
63 63 'created_on': '',
64 64 'enable_downloads': '',
65 65 'repo_id': '',
66 66 'user_id': '',
67 67 'enable_statistics': '',
68 68 'clone_uri': '',
69 69 'fork_id': '',
70 70 'group_id': '',
71 71 'created_by': '',
72 72 'repository': '',
73 73 'comment': '',
74 74 'commit': ''
75 75 })
76 76 def _comment_commit_repo_hook(*args, **kwargs):
77 77 """
78 78 POST CREATE REPOSITORY COMMENT ON COMMIT HOOK. This function will be executed after
79 79 a comment is made on this repository commit.
80 80
81 81 """
82 82 return HookResponse(0, '')
83 83
84 84
85 85 @has_kwargs({
86 'repo_name': '',
87 'repo_type': '',
88 'description': '',
89 'private': '',
90 'created_on': '',
91 'enable_downloads': '',
92 'repo_id': '',
93 'user_id': '',
94 'enable_statistics': '',
95 'clone_uri': '',
96 'fork_id': '',
97 'group_id': '',
98 'created_by': '',
99 'repository': '',
100 'comment': '',
101 'commit': ''
102 })
103 def _comment_edit_commit_repo_hook(*args, **kwargs):
104 """
105 POST CREATE REPOSITORY COMMENT ON COMMIT HOOK. This function will be executed after
106 a comment is made on this repository commit.
107
108 """
109 return HookResponse(0, '')
110
111
112 @has_kwargs({
86 113 'group_name': '',
87 114 'group_parent_id': '',
88 115 'group_description': '',
89 116 'group_id': '',
90 117 'user_id': '',
91 118 'created_by': '',
92 119 'created_on': '',
93 120 'enable_locking': ''
94 121 })
95 122 def _create_repo_group_hook(*args, **kwargs):
96 123 """
97 124 POST CREATE REPOSITORY GROUP HOOK, this function will be
98 125 executed after each repository group is created. kwargs available:
99 126 """
100 127 return HookResponse(0, '')
101 128
102 129
103 130 @has_kwargs({
104 131 'username': '',
105 132 'password': '',
106 133 'email': '',
107 134 'firstname': '',
108 135 'lastname': '',
109 136 'active': '',
110 137 'admin': '',
111 138 'created_by': '',
112 139 })
113 140 def _pre_create_user_hook(*args, **kwargs):
114 141 """
115 142 PRE CREATE USER HOOK, this function will be executed before each
116 143 user is created, it returns a tuple of bool, reason.
117 144 If bool is False the user creation will be stopped and reason
118 145 will be displayed to the user.
119 146
120 147 Return HookResponse(1, reason) to block user creation
121 148
122 149 """
123 150
124 151 reason = 'allowed'
125 152 return HookResponse(0, reason)
126 153
127 154
128 155 @has_kwargs({
129 156 'username': '',
130 157 'full_name_or_username': '',
131 158 'full_contact': '',
132 159 'user_id': '',
133 160 'name': '',
134 161 'firstname': '',
135 162 'short_contact': '',
136 163 'admin': '',
137 164 'lastname': '',
138 165 'ip_addresses': '',
139 166 'extern_type': '',
140 167 'extern_name': '',
141 168 'email': '',
142 169 'api_key': '',
143 170 'api_keys': '',
144 171 'last_login': '',
145 172 'full_name': '',
146 173 'active': '',
147 174 'password': '',
148 175 'emails': '',
149 176 'inherit_default_permissions': '',
150 177 'created_by': '',
151 178 'created_on': '',
152 179 })
153 180 def _create_user_hook(*args, **kwargs):
154 181 """
155 182 POST CREATE USER HOOK, this function will be executed after each user is created
156 183 """
157 184 return HookResponse(0, '')
158 185
159 186
160 187 @has_kwargs({
161 188 'repo_name': '',
162 189 'repo_type': '',
163 190 'description': '',
164 191 'private': '',
165 192 'created_on': '',
166 193 'enable_downloads': '',
167 194 'repo_id': '',
168 195 'user_id': '',
169 196 'enable_statistics': '',
170 197 'clone_uri': '',
171 198 'fork_id': '',
172 199 'group_id': '',
173 200 'deleted_by': '',
174 201 'deleted_on': '',
175 202 })
176 203 def _delete_repo_hook(*args, **kwargs):
177 204 """
178 205 POST DELETE REPOSITORY HOOK, this function will be executed after
179 206 each repository deletion
180 207 """
181 208 return HookResponse(0, '')
182 209
183 210
184 211 @has_kwargs({
185 212 'username': '',
186 213 'full_name_or_username': '',
187 214 'full_contact': '',
188 215 'user_id': '',
189 216 'name': '',
190 217 'short_contact': '',
191 218 'admin': '',
192 219 'firstname': '',
193 220 'lastname': '',
194 221 'ip_addresses': '',
195 222 'email': '',
196 223 'api_key': '',
197 224 'last_login': '',
198 225 'full_name': '',
199 226 'active': '',
200 227 'password': '',
201 228 'emails': '',
202 229 'inherit_default_permissions': '',
203 230 'deleted_by': '',
204 231 })
205 232 def _delete_user_hook(*args, **kwargs):
206 233 """
207 234 POST DELETE USER HOOK, this function will be executed after each
208 235 user is deleted kwargs available:
209 236 """
210 237 return HookResponse(0, '')
211 238
212 239
213 240 # =============================================================================
214 241 # PUSH/PULL RELATED HOOKS
215 242 # =============================================================================
216 243 @has_kwargs({
217 244 'server_url': 'url of instance that triggered this hook',
218 245 'config': 'path to .ini config used',
219 246 'scm': 'type of version control "git", "hg", "svn"',
220 247 'username': 'username of actor who triggered this event',
221 248 'ip': 'ip address of actor who triggered this hook',
222 249 'action': '',
223 250 'repository': 'repository name',
224 251 'repo_store_path': 'full path to where repositories are stored',
225 252 'commit_ids': 'pre transaction metadata for commit ids',
226 253 'hook_type': '',
227 254 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
228 255 })
229 256 def _pre_push_hook(*args, **kwargs):
230 257 """
231 258 Post push hook
232 259 To stop version control from storing the transaction and send a message to user
233 260 use non-zero HookResponse with a message, e.g return HookResponse(1, 'Not allowed')
234 261
235 262 This message will be shown back to client during PUSH operation
236 263
237 264 Commit ids might look like that::
238 265
239 266 [{u'hg_env|git_env': ...,
240 267 u'multiple_heads': [],
241 268 u'name': u'default',
242 269 u'new_rev': u'd0befe0692e722e01d5677f27a104631cf798b69',
243 270 u'old_rev': u'd0befe0692e722e01d5677f27a104631cf798b69',
244 271 u'ref': u'',
245 272 u'total_commits': 2,
246 273 u'type': u'branch'}]
247 274 """
248 275 return HookResponse(0, '')
249 276
250 277
251 278 @has_kwargs({
252 279 'server_url': 'url of instance that triggered this hook',
253 280 'config': 'path to .ini config used',
254 281 'scm': 'type of version control "git", "hg", "svn"',
255 282 'username': 'username of actor who triggered this event',
256 283 'ip': 'ip address of actor who triggered this hook',
257 284 'action': '',
258 285 'repository': 'repository name',
259 286 'repo_store_path': 'full path to where repositories are stored',
260 287 'commit_ids': 'list of pushed commit_ids (sha1)',
261 288 'hook_type': '',
262 289 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
263 290 })
264 291 def _push_hook(*args, **kwargs):
265 292 """
266 293 POST PUSH HOOK, this function will be executed after each push it's
267 294 executed after the build-in hook that RhodeCode uses for logging pushes
268 295 """
269 296 return HookResponse(0, '')
270 297
271 298
272 299 @has_kwargs({
273 300 'server_url': 'url of instance that triggered this hook',
274 301 'repo_store_path': 'full path to where repositories are stored',
275 302 'config': 'path to .ini config used',
276 303 'scm': 'type of version control "git", "hg", "svn"',
277 304 'username': 'username of actor who triggered this event',
278 305 'ip': 'ip address of actor who triggered this hook',
279 306 'action': '',
280 307 'repository': 'repository name',
281 308 'hook_type': '',
282 309 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
283 310 })
284 311 def _pre_pull_hook(*args, **kwargs):
285 312 """
286 313 Post pull hook
287 314 """
288 315 return HookResponse(0, '')
289 316
290 317
291 318 @has_kwargs({
292 319 'server_url': 'url of instance that triggered this hook',
293 320 'repo_store_path': 'full path to where repositories are stored',
294 321 'config': 'path to .ini config used',
295 322 'scm': 'type of version control "git", "hg", "svn"',
296 323 'username': 'username of actor who triggered this event',
297 324 'ip': 'ip address of actor who triggered this hook',
298 325 'action': '',
299 326 'repository': 'repository name',
300 327 'hook_type': '',
301 328 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
302 329 })
303 330 def _pull_hook(*args, **kwargs):
304 331 """
305 332 This hook will be executed after each code pull.
306 333 """
307 334 return HookResponse(0, '')
308 335
309 336
310 337 # =============================================================================
311 338 # PULL REQUEST RELATED HOOKS
312 339 # =============================================================================
313 340 @has_kwargs({
314 341 'server_url': 'url of instance that triggered this hook',
315 342 'config': 'path to .ini config used',
316 343 'scm': 'type of version control "git", "hg", "svn"',
317 344 'username': 'username of actor who triggered this event',
318 345 'ip': 'ip address of actor who triggered this hook',
319 346 'action': '',
320 347 'repository': 'repository name',
321 348 'pull_request_id': '',
322 349 'url': '',
323 350 'title': '',
324 351 'description': '',
325 352 'status': '',
326 353 'created_on': '',
327 354 'updated_on': '',
328 355 'commit_ids': '',
329 356 'review_status': '',
330 357 'mergeable': '',
331 358 'source': '',
332 359 'target': '',
333 360 'author': '',
334 361 'reviewers': '',
335 362 })
336 363 def _create_pull_request_hook(*args, **kwargs):
337 364 """
338 365 This hook will be executed after creation of a pull request.
339 366 """
340 367 return HookResponse(0, '')
341 368
342 369
343 370 @has_kwargs({
344 371 'server_url': 'url of instance that triggered this hook',
345 372 'config': 'path to .ini config used',
346 373 'scm': 'type of version control "git", "hg", "svn"',
347 374 'username': 'username of actor who triggered this event',
348 375 'ip': 'ip address of actor who triggered this hook',
349 376 'action': '',
350 377 'repository': 'repository name',
351 378 'pull_request_id': '',
352 379 'url': '',
353 380 'title': '',
354 381 'description': '',
355 382 'status': '',
356 383 'created_on': '',
357 384 'updated_on': '',
358 385 'commit_ids': '',
359 386 'review_status': '',
360 387 'mergeable': '',
361 388 'source': '',
362 389 'target': '',
363 390 'author': '',
364 391 'reviewers': '',
365 392 })
366 393 def _review_pull_request_hook(*args, **kwargs):
367 394 """
368 395 This hook will be executed after review action was made on a pull request.
369 396 """
370 397 return HookResponse(0, '')
371 398
372 399
373 400 @has_kwargs({
374 401 'server_url': 'url of instance that triggered this hook',
375 402 'config': 'path to .ini config used',
376 403 'scm': 'type of version control "git", "hg", "svn"',
377 404 'username': 'username of actor who triggered this event',
378 405 'ip': 'ip address of actor who triggered this hook',
379 406
380 407 'action': '',
381 408 'repository': 'repository name',
382 409 'pull_request_id': '',
383 410 'url': '',
384 411 'title': '',
385 412 'description': '',
386 413 'status': '',
387 414 'comment': '',
388 415 'created_on': '',
389 416 'updated_on': '',
390 417 'commit_ids': '',
391 418 'review_status': '',
392 419 'mergeable': '',
393 420 'source': '',
394 421 'target': '',
395 422 'author': '',
396 423 'reviewers': '',
397 424 })
398 425 def _comment_pull_request_hook(*args, **kwargs):
399 426 """
400 427 This hook will be executed after comment is made on a pull request
401 428 """
402 429 return HookResponse(0, '')
403 430
404 431
405 432 @has_kwargs({
406 433 'server_url': 'url of instance that triggered this hook',
407 434 'config': 'path to .ini config used',
408 435 'scm': 'type of version control "git", "hg", "svn"',
409 436 'username': 'username of actor who triggered this event',
410 437 'ip': 'ip address of actor who triggered this hook',
438
439 'action': '',
440 'repository': 'repository name',
441 'pull_request_id': '',
442 'url': '',
443 'title': '',
444 'description': '',
445 'status': '',
446 'comment': '',
447 'created_on': '',
448 'updated_on': '',
449 'commit_ids': '',
450 'review_status': '',
451 'mergeable': '',
452 'source': '',
453 'target': '',
454 'author': '',
455 'reviewers': '',
456 })
457 def _comment_edit_pull_request_hook(*args, **kwargs):
458 """
459 This hook will be executed after comment is made on a pull request
460 """
461 return HookResponse(0, '')
462
463
464 @has_kwargs({
465 'server_url': 'url of instance that triggered this hook',
466 'config': 'path to .ini config used',
467 'scm': 'type of version control "git", "hg", "svn"',
468 'username': 'username of actor who triggered this event',
469 'ip': 'ip address of actor who triggered this hook',
411 470 'action': '',
412 471 'repository': 'repository name',
413 472 'pull_request_id': '',
414 473 'url': '',
415 474 'title': '',
416 475 'description': '',
417 476 'status': '',
418 477 'created_on': '',
419 478 'updated_on': '',
420 479 'commit_ids': '',
421 480 'review_status': '',
422 481 'mergeable': '',
423 482 'source': '',
424 483 'target': '',
425 484 'author': '',
426 485 'reviewers': '',
427 486 })
428 487 def _update_pull_request_hook(*args, **kwargs):
429 488 """
430 489 This hook will be executed after pull requests has been updated with new commits.
431 490 """
432 491 return HookResponse(0, '')
433 492
434 493
435 494 @has_kwargs({
436 495 'server_url': 'url of instance that triggered this hook',
437 496 'config': 'path to .ini config used',
438 497 'scm': 'type of version control "git", "hg", "svn"',
439 498 'username': 'username of actor who triggered this event',
440 499 'ip': 'ip address of actor who triggered this hook',
441 500 'action': '',
442 501 'repository': 'repository name',
443 502 'pull_request_id': '',
444 503 'url': '',
445 504 'title': '',
446 505 'description': '',
447 506 'status': '',
448 507 'created_on': '',
449 508 'updated_on': '',
450 509 'commit_ids': '',
451 510 'review_status': '',
452 511 'mergeable': '',
453 512 'source': '',
454 513 'target': '',
455 514 'author': '',
456 515 'reviewers': '',
457 516 })
458 517 def _merge_pull_request_hook(*args, **kwargs):
459 518 """
460 519 This hook will be executed after merge of a pull request.
461 520 """
462 521 return HookResponse(0, '')
463 522
464 523
465 524 @has_kwargs({
466 525 'server_url': 'url of instance that triggered this hook',
467 526 'config': 'path to .ini config used',
468 527 'scm': 'type of version control "git", "hg", "svn"',
469 528 'username': 'username of actor who triggered this event',
470 529 'ip': 'ip address of actor who triggered this hook',
471 530 'action': '',
472 531 'repository': 'repository name',
473 532 'pull_request_id': '',
474 533 'url': '',
475 534 'title': '',
476 535 'description': '',
477 536 'status': '',
478 537 'created_on': '',
479 538 'updated_on': '',
480 539 'commit_ids': '',
481 540 'review_status': '',
482 541 'mergeable': '',
483 542 'source': '',
484 543 'target': '',
485 544 'author': '',
486 545 'reviewers': '',
487 546 })
488 547 def _close_pull_request_hook(*args, **kwargs):
489 548 """
490 549 This hook will be executed after close of a pull request.
491 550 """
492 551 return HookResponse(0, '')
@@ -1,366 +1,366 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode task modules, containing all task that suppose to be run
23 23 by celery daemon
24 24 """
25 25
26 26 import os
27 27 import time
28 28
29 29 from pyramid import compat
30 30 from pyramid_mailer.mailer import Mailer
31 31 from pyramid_mailer.message import Message
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 from rhodecode.lib.hooks_base import log_create_repository
36 from rhodecode.lib import hooks_base
37 37 from rhodecode.lib.utils2 import safe_int, str2bool
38 38 from rhodecode.model.db import (
39 39 Session, IntegrityError, true, Repository, RepoGroup, User)
40 40
41 41
42 42 @async_task(ignore_result=True, base=RequestContextTask)
43 43 def send_email(recipients, subject, body='', html_body='', email_config=None):
44 44 """
45 45 Sends an email with defined parameters from the .ini files.
46 46
47 47 :param recipients: list of recipients, it this is empty the defined email
48 48 address from field 'email_to' is used instead
49 49 :param subject: subject of the mail
50 50 :param body: body of the mail
51 51 :param html_body: html version of body
52 52 :param email_config: specify custom configuration for mailer
53 53 """
54 54 log = get_logger(send_email)
55 55
56 56 email_config = email_config or rhodecode.CONFIG
57 57
58 58 mail_server = email_config.get('smtp_server') or None
59 59 if mail_server is None:
60 60 log.error("SMTP server information missing. Sending email failed. "
61 61 "Make sure that `smtp_server` variable is configured "
62 62 "inside the .ini file")
63 63 return False
64 64
65 65 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
66 66
67 67 if recipients:
68 68 if isinstance(recipients, compat.string_types):
69 69 recipients = recipients.split(',')
70 70 else:
71 71 # if recipients are not defined we send to email_config + all admins
72 72 admins = []
73 73 for u in User.query().filter(User.admin == true()).all():
74 74 if u.email:
75 75 admins.append(u.email)
76 76 recipients = []
77 77 config_email = email_config.get('email_to')
78 78 if config_email:
79 79 recipients += [config_email]
80 80 recipients += admins
81 81
82 82 # translate our LEGACY config into the one that pyramid_mailer supports
83 83 email_conf = dict(
84 84 host=mail_server,
85 85 port=email_config.get('smtp_port', 25),
86 86 username=email_config.get('smtp_username'),
87 87 password=email_config.get('smtp_password'),
88 88
89 89 tls=str2bool(email_config.get('smtp_use_tls')),
90 90 ssl=str2bool(email_config.get('smtp_use_ssl')),
91 91
92 92 # SSL key file
93 93 # keyfile='',
94 94
95 95 # SSL certificate file
96 96 # certfile='',
97 97
98 98 # Location of maildir
99 99 # queue_path='',
100 100
101 101 default_sender=email_config.get('app_email_from', 'RhodeCode'),
102 102
103 103 debug=str2bool(email_config.get('smtp_debug')),
104 104 # /usr/sbin/sendmail Sendmail executable
105 105 # sendmail_app='',
106 106
107 107 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
108 108 # sendmail_template='',
109 109 )
110 110
111 111 try:
112 112 mailer = Mailer(**email_conf)
113 113
114 114 message = Message(subject=subject,
115 115 sender=email_conf['default_sender'],
116 116 recipients=recipients,
117 117 body=body, html=html_body)
118 118 mailer.send_immediately(message)
119 119
120 120 except Exception:
121 121 log.exception('Mail sending failed')
122 122 return False
123 123 return True
124 124
125 125
126 126 @async_task(ignore_result=True, base=RequestContextTask)
127 127 def create_repo(form_data, cur_user):
128 128 from rhodecode.model.repo import RepoModel
129 129 from rhodecode.model.user import UserModel
130 130 from rhodecode.model.scm import ScmModel
131 131 from rhodecode.model.settings import SettingsModel
132 132
133 133 log = get_logger(create_repo)
134 134
135 135 cur_user = UserModel()._get_user(cur_user)
136 136 owner = cur_user
137 137
138 138 repo_name = form_data['repo_name']
139 139 repo_name_full = form_data['repo_name_full']
140 140 repo_type = form_data['repo_type']
141 141 description = form_data['repo_description']
142 142 private = form_data['repo_private']
143 143 clone_uri = form_data.get('clone_uri')
144 144 repo_group = safe_int(form_data['repo_group'])
145 145 copy_fork_permissions = form_data.get('copy_permissions')
146 146 copy_group_permissions = form_data.get('repo_copy_permissions')
147 147 fork_of = form_data.get('fork_parent_id')
148 148 state = form_data.get('repo_state', Repository.STATE_PENDING)
149 149
150 150 # repo creation defaults, private and repo_type are filled in form
151 151 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
152 152 enable_statistics = form_data.get(
153 153 'enable_statistics', defs.get('repo_enable_statistics'))
154 154 enable_locking = form_data.get(
155 155 'enable_locking', defs.get('repo_enable_locking'))
156 156 enable_downloads = form_data.get(
157 157 'enable_downloads', defs.get('repo_enable_downloads'))
158 158
159 159 # set landing rev based on default branches for SCM
160 160 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
161 161
162 162 try:
163 163 RepoModel()._create_repo(
164 164 repo_name=repo_name_full,
165 165 repo_type=repo_type,
166 166 description=description,
167 167 owner=owner,
168 168 private=private,
169 169 clone_uri=clone_uri,
170 170 repo_group=repo_group,
171 171 landing_rev=landing_ref,
172 172 fork_of=fork_of,
173 173 copy_fork_permissions=copy_fork_permissions,
174 174 copy_group_permissions=copy_group_permissions,
175 175 enable_statistics=enable_statistics,
176 176 enable_locking=enable_locking,
177 177 enable_downloads=enable_downloads,
178 178 state=state
179 179 )
180 180 Session().commit()
181 181
182 182 # now create this repo on Filesystem
183 183 RepoModel()._create_filesystem_repo(
184 184 repo_name=repo_name,
185 185 repo_type=repo_type,
186 186 repo_group=RepoModel()._get_repo_group(repo_group),
187 187 clone_uri=clone_uri,
188 188 )
189 189 repo = Repository.get_by_repo_name(repo_name_full)
190 log_create_repository(created_by=owner.username, **repo.get_dict())
190 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
191 191
192 192 # update repo commit caches initially
193 193 repo.update_commit_cache()
194 194
195 195 # set new created state
196 196 repo.set_state(Repository.STATE_CREATED)
197 197 repo_id = repo.repo_id
198 198 repo_data = repo.get_api_data()
199 199
200 200 audit_logger.store(
201 201 'repo.create', action_data={'data': repo_data},
202 202 user=cur_user,
203 203 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
204 204
205 205 Session().commit()
206 206 except Exception as e:
207 207 log.warning('Exception occurred when creating repository, '
208 208 'doing cleanup...', exc_info=True)
209 209 if isinstance(e, IntegrityError):
210 210 Session().rollback()
211 211
212 212 # rollback things manually !
213 213 repo = Repository.get_by_repo_name(repo_name_full)
214 214 if repo:
215 215 Repository.delete(repo.repo_id)
216 216 Session().commit()
217 217 RepoModel()._delete_filesystem_repo(repo)
218 218 log.info('Cleanup of repo %s finished', repo_name_full)
219 219 raise
220 220
221 221 return True
222 222
223 223
224 224 @async_task(ignore_result=True, base=RequestContextTask)
225 225 def create_repo_fork(form_data, cur_user):
226 226 """
227 227 Creates a fork of repository using internal VCS methods
228 228 """
229 229 from rhodecode.model.repo import RepoModel
230 230 from rhodecode.model.user import UserModel
231 231
232 232 log = get_logger(create_repo_fork)
233 233
234 234 cur_user = UserModel()._get_user(cur_user)
235 235 owner = cur_user
236 236
237 237 repo_name = form_data['repo_name'] # fork in this case
238 238 repo_name_full = form_data['repo_name_full']
239 239 repo_type = form_data['repo_type']
240 240 description = form_data['description']
241 241 private = form_data['private']
242 242 clone_uri = form_data.get('clone_uri')
243 243 repo_group = safe_int(form_data['repo_group'])
244 244 landing_ref = form_data['landing_rev']
245 245 copy_fork_permissions = form_data.get('copy_permissions')
246 246 fork_id = safe_int(form_data.get('fork_parent_id'))
247 247
248 248 try:
249 249 fork_of = RepoModel()._get_repo(fork_id)
250 250 RepoModel()._create_repo(
251 251 repo_name=repo_name_full,
252 252 repo_type=repo_type,
253 253 description=description,
254 254 owner=owner,
255 255 private=private,
256 256 clone_uri=clone_uri,
257 257 repo_group=repo_group,
258 258 landing_rev=landing_ref,
259 259 fork_of=fork_of,
260 260 copy_fork_permissions=copy_fork_permissions
261 261 )
262 262
263 263 Session().commit()
264 264
265 265 base_path = Repository.base_path()
266 266 source_repo_path = os.path.join(base_path, fork_of.repo_name)
267 267
268 268 # now create this repo on Filesystem
269 269 RepoModel()._create_filesystem_repo(
270 270 repo_name=repo_name,
271 271 repo_type=repo_type,
272 272 repo_group=RepoModel()._get_repo_group(repo_group),
273 273 clone_uri=source_repo_path,
274 274 )
275 275 repo = Repository.get_by_repo_name(repo_name_full)
276 log_create_repository(created_by=owner.username, **repo.get_dict())
276 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
277 277
278 278 # update repo commit caches initially
279 279 config = repo._config
280 280 config.set('extensions', 'largefiles', '')
281 281 repo.update_commit_cache(config=config)
282 282
283 283 # set new created state
284 284 repo.set_state(Repository.STATE_CREATED)
285 285
286 286 repo_id = repo.repo_id
287 287 repo_data = repo.get_api_data()
288 288 audit_logger.store(
289 289 'repo.fork', action_data={'data': repo_data},
290 290 user=cur_user,
291 291 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
292 292
293 293 Session().commit()
294 294 except Exception as e:
295 295 log.warning('Exception occurred when forking repository, '
296 296 'doing cleanup...', exc_info=True)
297 297 if isinstance(e, IntegrityError):
298 298 Session().rollback()
299 299
300 300 # rollback things manually !
301 301 repo = Repository.get_by_repo_name(repo_name_full)
302 302 if repo:
303 303 Repository.delete(repo.repo_id)
304 304 Session().commit()
305 305 RepoModel()._delete_filesystem_repo(repo)
306 306 log.info('Cleanup of repo %s finished', repo_name_full)
307 307 raise
308 308
309 309 return True
310 310
311 311
312 312 @async_task(ignore_result=True)
313 313 def repo_maintenance(repoid):
314 314 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
315 315 log = get_logger(repo_maintenance)
316 316 repo = Repository.get_by_id_or_repo_name(repoid)
317 317 if repo:
318 318 maintenance = repo_maintenance_lib.RepoMaintenance()
319 319 tasks = maintenance.get_tasks_for_repo(repo)
320 320 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
321 321 executed_types = maintenance.execute(repo)
322 322 log.debug('Got execution results %s', executed_types)
323 323 else:
324 324 log.debug('Repo `%s` not found or without a clone_url', repoid)
325 325
326 326
327 327 @async_task(ignore_result=True)
328 328 def check_for_update():
329 329 from rhodecode.model.update import UpdateModel
330 330 update_url = UpdateModel().get_update_url()
331 331 cur_ver = rhodecode.__version__
332 332
333 333 try:
334 334 data = UpdateModel().get_update_data(update_url)
335 335 latest = data['versions'][0]
336 336 UpdateModel().store_version(latest['version'])
337 337 except Exception:
338 338 pass
339 339
340 340
341 341 @async_task(ignore_result=False)
342 342 def beat_check(*args, **kwargs):
343 343 log = get_logger(beat_check)
344 344 log.info('Got args: %r and kwargs %r', args, kwargs)
345 345 return time.time()
346 346
347 347
348 348 @async_task(ignore_result=True)
349 349 def sync_last_update(*args, **kwargs):
350 350
351 351 skip_repos = kwargs.get('skip_repos')
352 352 if not skip_repos:
353 353 repos = Repository.query() \
354 354 .order_by(Repository.group_id.asc())
355 355
356 356 for repo in repos:
357 357 repo.update_commit_cache()
358 358
359 359 skip_groups = kwargs.get('skip_groups')
360 360 if not skip_groups:
361 361 repo_groups = RepoGroup.query() \
362 362 .filter(RepoGroup.group_parent_id == None)
363 363
364 364 for root_gr in repo_groups:
365 365 for repo_gr in reversed(root_gr.recursive_groups()):
366 366 repo_gr.update_commit_cache()
@@ -1,509 +1,526 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of hooks run by RhodeCode Enterprise
24 24 """
25 25
26 26 import os
27 27 import logging
28 28
29 29 import rhodecode
30 30 from rhodecode import events
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.utils2 import safe_str
34 34 from rhodecode.lib.exceptions import (
35 35 HTTPLockedRC, HTTPBranchProtected, UserCreationError)
36 36 from rhodecode.model.db import Repository, User
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class HookResponse(object):
42 42 def __init__(self, status, output):
43 43 self.status = status
44 44 self.output = output
45 45
46 46 def __add__(self, other):
47 47 other_status = getattr(other, 'status', 0)
48 48 new_status = max(self.status, other_status)
49 49 other_output = getattr(other, 'output', '')
50 50 new_output = self.output + other_output
51 51
52 52 return HookResponse(new_status, new_output)
53 53
54 54 def __bool__(self):
55 55 return self.status == 0
56 56
57 57
58 58 def is_shadow_repo(extras):
59 59 """
60 60 Returns ``True`` if this is an action executed against a shadow repository.
61 61 """
62 62 return extras['is_shadow_repo']
63 63
64 64
65 65 def _get_scm_size(alias, root_path):
66 66
67 67 if not alias.startswith('.'):
68 68 alias += '.'
69 69
70 70 size_scm, size_root = 0, 0
71 71 for path, unused_dirs, files in os.walk(safe_str(root_path)):
72 72 if path.find(alias) != -1:
73 73 for f in files:
74 74 try:
75 75 size_scm += os.path.getsize(os.path.join(path, f))
76 76 except OSError:
77 77 pass
78 78 else:
79 79 for f in files:
80 80 try:
81 81 size_root += os.path.getsize(os.path.join(path, f))
82 82 except OSError:
83 83 pass
84 84
85 85 size_scm_f = h.format_byte_size_binary(size_scm)
86 86 size_root_f = h.format_byte_size_binary(size_root)
87 87 size_total_f = h.format_byte_size_binary(size_root + size_scm)
88 88
89 89 return size_scm_f, size_root_f, size_total_f
90 90
91 91
92 92 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
93 93 def repo_size(extras):
94 94 """Present size of repository after push."""
95 95 repo = Repository.get_by_repo_name(extras.repository)
96 96 vcs_part = safe_str(u'.%s' % repo.repo_type)
97 97 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
98 98 repo.repo_full_path)
99 99 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
100 100 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
101 101 return HookResponse(0, msg)
102 102
103 103
104 104 def pre_push(extras):
105 105 """
106 106 Hook executed before pushing code.
107 107
108 108 It bans pushing when the repository is locked.
109 109 """
110 110
111 111 user = User.get_by_username(extras.username)
112 112 output = ''
113 113 if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
114 114 locked_by = User.get(extras.locked_by[0]).username
115 115 reason = extras.locked_by[2]
116 116 # this exception is interpreted in git/hg middlewares and based
117 117 # on that proper return code is server to client
118 118 _http_ret = HTTPLockedRC(
119 119 _locked_by_explanation(extras.repository, locked_by, reason))
120 120 if str(_http_ret.code).startswith('2'):
121 121 # 2xx Codes don't raise exceptions
122 122 output = _http_ret.title
123 123 else:
124 124 raise _http_ret
125 125
126 126 hook_response = ''
127 127 if not is_shadow_repo(extras):
128 128 if extras.commit_ids and extras.check_branch_perms:
129 129
130 130 auth_user = user.AuthUser()
131 131 repo = Repository.get_by_repo_name(extras.repository)
132 132 affected_branches = []
133 133 if repo.repo_type == 'hg':
134 134 for entry in extras.commit_ids:
135 135 if entry['type'] == 'branch':
136 136 is_forced = bool(entry['multiple_heads'])
137 137 affected_branches.append([entry['name'], is_forced])
138 138 elif repo.repo_type == 'git':
139 139 for entry in extras.commit_ids:
140 140 if entry['type'] == 'heads':
141 141 is_forced = bool(entry['pruned_sha'])
142 142 affected_branches.append([entry['name'], is_forced])
143 143
144 144 for branch_name, is_forced in affected_branches:
145 145
146 146 rule, branch_perm = auth_user.get_rule_and_branch_permission(
147 147 extras.repository, branch_name)
148 148 if not branch_perm:
149 149 # no branch permission found for this branch, just keep checking
150 150 continue
151 151
152 152 if branch_perm == 'branch.push_force':
153 153 continue
154 154 elif branch_perm == 'branch.push' and is_forced is False:
155 155 continue
156 156 elif branch_perm == 'branch.push' and is_forced is True:
157 157 halt_message = 'Branch `{}` changes rejected by rule {}. ' \
158 158 'FORCE PUSH FORBIDDEN.'.format(branch_name, rule)
159 159 else:
160 160 halt_message = 'Branch `{}` changes rejected by rule {}.'.format(
161 161 branch_name, rule)
162 162
163 163 if halt_message:
164 164 _http_ret = HTTPBranchProtected(halt_message)
165 165 raise _http_ret
166 166
167 167 # Propagate to external components. This is done after checking the
168 168 # lock, for consistent behavior.
169 169 hook_response = pre_push_extension(
170 170 repo_store_path=Repository.base_path(), **extras)
171 171 events.trigger(events.RepoPrePushEvent(
172 172 repo_name=extras.repository, extras=extras))
173 173
174 174 return HookResponse(0, output) + hook_response
175 175
176 176
177 177 def pre_pull(extras):
178 178 """
179 179 Hook executed before pulling the code.
180 180
181 181 It bans pulling when the repository is locked.
182 182 """
183 183
184 184 output = ''
185 185 if extras.locked_by[0]:
186 186 locked_by = User.get(extras.locked_by[0]).username
187 187 reason = extras.locked_by[2]
188 188 # this exception is interpreted in git/hg middlewares and based
189 189 # on that proper return code is server to client
190 190 _http_ret = HTTPLockedRC(
191 191 _locked_by_explanation(extras.repository, locked_by, reason))
192 192 if str(_http_ret.code).startswith('2'):
193 193 # 2xx Codes don't raise exceptions
194 194 output = _http_ret.title
195 195 else:
196 196 raise _http_ret
197 197
198 198 # Propagate to external components. This is done after checking the
199 199 # lock, for consistent behavior.
200 200 hook_response = ''
201 201 if not is_shadow_repo(extras):
202 202 extras.hook_type = extras.hook_type or 'pre_pull'
203 203 hook_response = pre_pull_extension(
204 204 repo_store_path=Repository.base_path(), **extras)
205 205 events.trigger(events.RepoPrePullEvent(
206 206 repo_name=extras.repository, extras=extras))
207 207
208 208 return HookResponse(0, output) + hook_response
209 209
210 210
211 211 def post_pull(extras):
212 212 """Hook executed after client pulls the code."""
213 213
214 214 audit_user = audit_logger.UserWrap(
215 215 username=extras.username,
216 216 ip_addr=extras.ip)
217 217 repo = audit_logger.RepoWrap(repo_name=extras.repository)
218 218 audit_logger.store(
219 219 'user.pull', action_data={'user_agent': extras.user_agent},
220 220 user=audit_user, repo=repo, commit=True)
221 221
222 222 output = ''
223 223 # make lock is a tri state False, True, None. We only make lock on True
224 224 if extras.make_lock is True and not is_shadow_repo(extras):
225 225 user = User.get_by_username(extras.username)
226 226 Repository.lock(Repository.get_by_repo_name(extras.repository),
227 227 user.user_id,
228 228 lock_reason=Repository.LOCK_PULL)
229 229 msg = 'Made lock on repo `%s`' % (extras.repository,)
230 230 output += msg
231 231
232 232 if extras.locked_by[0]:
233 233 locked_by = User.get(extras.locked_by[0]).username
234 234 reason = extras.locked_by[2]
235 235 _http_ret = HTTPLockedRC(
236 236 _locked_by_explanation(extras.repository, locked_by, reason))
237 237 if str(_http_ret.code).startswith('2'):
238 238 # 2xx Codes don't raise exceptions
239 239 output += _http_ret.title
240 240
241 241 # Propagate to external components.
242 242 hook_response = ''
243 243 if not is_shadow_repo(extras):
244 244 extras.hook_type = extras.hook_type or 'post_pull'
245 245 hook_response = post_pull_extension(
246 246 repo_store_path=Repository.base_path(), **extras)
247 247 events.trigger(events.RepoPullEvent(
248 248 repo_name=extras.repository, extras=extras))
249 249
250 250 return HookResponse(0, output) + hook_response
251 251
252 252
253 253 def post_push(extras):
254 254 """Hook executed after user pushes to the repository."""
255 255 commit_ids = extras.commit_ids
256 256
257 257 # log the push call
258 258 audit_user = audit_logger.UserWrap(
259 259 username=extras.username, ip_addr=extras.ip)
260 260 repo = audit_logger.RepoWrap(repo_name=extras.repository)
261 261 audit_logger.store(
262 262 'user.push', action_data={
263 263 'user_agent': extras.user_agent,
264 264 'commit_ids': commit_ids[:400]},
265 265 user=audit_user, repo=repo, commit=True)
266 266
267 267 # Propagate to external components.
268 268 output = ''
269 269 # make lock is a tri state False, True, None. We only release lock on False
270 270 if extras.make_lock is False and not is_shadow_repo(extras):
271 271 Repository.unlock(Repository.get_by_repo_name(extras.repository))
272 272 msg = 'Released lock on repo `{}`\n'.format(safe_str(extras.repository))
273 273 output += msg
274 274
275 275 if extras.locked_by[0]:
276 276 locked_by = User.get(extras.locked_by[0]).username
277 277 reason = extras.locked_by[2]
278 278 _http_ret = HTTPLockedRC(
279 279 _locked_by_explanation(extras.repository, locked_by, reason))
280 280 # TODO: johbo: if not?
281 281 if str(_http_ret.code).startswith('2'):
282 282 # 2xx Codes don't raise exceptions
283 283 output += _http_ret.title
284 284
285 285 if extras.new_refs:
286 286 tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format(
287 287 safe_str(extras.server_url), safe_str(extras.repository))
288 288
289 289 for branch_name in extras.new_refs['branches']:
290 290 output += 'RhodeCode: open pull request link: {}\n'.format(
291 291 tmpl.format(ref_type='branch', ref_name=safe_str(branch_name)))
292 292
293 293 for book_name in extras.new_refs['bookmarks']:
294 294 output += 'RhodeCode: open pull request link: {}\n'.format(
295 295 tmpl.format(ref_type='bookmark', ref_name=safe_str(book_name)))
296 296
297 297 hook_response = ''
298 298 if not is_shadow_repo(extras):
299 299 hook_response = post_push_extension(
300 300 repo_store_path=Repository.base_path(),
301 301 **extras)
302 302 events.trigger(events.RepoPushEvent(
303 303 repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
304 304
305 305 output += 'RhodeCode: push completed\n'
306 306 return HookResponse(0, output) + hook_response
307 307
308 308
309 309 def _locked_by_explanation(repo_name, user_name, reason):
310 310 message = (
311 311 'Repository `%s` locked by user `%s`. Reason:`%s`'
312 312 % (repo_name, user_name, reason))
313 313 return message
314 314
315 315
316 316 def check_allowed_create_user(user_dict, created_by, **kwargs):
317 317 # pre create hooks
318 318 if pre_create_user.is_active():
319 319 hook_result = pre_create_user(created_by=created_by, **user_dict)
320 320 allowed = hook_result.status == 0
321 321 if not allowed:
322 322 reason = hook_result.output
323 323 raise UserCreationError(reason)
324 324
325 325
326 326 class ExtensionCallback(object):
327 327 """
328 328 Forwards a given call to rcextensions, sanitizes keyword arguments.
329 329
330 330 Does check if there is an extension active for that hook. If it is
331 331 there, it will forward all `kwargs_keys` keyword arguments to the
332 332 extension callback.
333 333 """
334 334
335 335 def __init__(self, hook_name, kwargs_keys):
336 336 self._hook_name = hook_name
337 337 self._kwargs_keys = set(kwargs_keys)
338 338
339 339 def __call__(self, *args, **kwargs):
340 340 log.debug('Calling extension callback for `%s`', self._hook_name)
341 341 callback = self._get_callback()
342 342 if not callback:
343 343 log.debug('extension callback `%s` not found, skipping...', self._hook_name)
344 344 return
345 345
346 346 kwargs_to_pass = {}
347 347 for key in self._kwargs_keys:
348 348 try:
349 349 kwargs_to_pass[key] = kwargs[key]
350 350 except KeyError:
351 351 log.error('Failed to fetch %s key from given kwargs. '
352 352 'Expected keys: %s', key, self._kwargs_keys)
353 353 raise
354 354
355 355 # backward compat for removed api_key for old hooks. This was it works
356 356 # with older rcextensions that require api_key present
357 357 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
358 358 kwargs_to_pass['api_key'] = '_DEPRECATED_'
359 359 return callback(**kwargs_to_pass)
360 360
361 361 def is_active(self):
362 362 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
363 363
364 364 def _get_callback(self):
365 365 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
366 366
367 367
368 368 pre_pull_extension = ExtensionCallback(
369 369 hook_name='PRE_PULL_HOOK',
370 370 kwargs_keys=(
371 371 'server_url', 'config', 'scm', 'username', 'ip', 'action',
372 372 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
373 373
374 374
375 375 post_pull_extension = ExtensionCallback(
376 376 hook_name='PULL_HOOK',
377 377 kwargs_keys=(
378 378 'server_url', 'config', 'scm', 'username', 'ip', 'action',
379 379 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
380 380
381 381
382 382 pre_push_extension = ExtensionCallback(
383 383 hook_name='PRE_PUSH_HOOK',
384 384 kwargs_keys=(
385 385 'server_url', 'config', 'scm', 'username', 'ip', 'action',
386 386 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
387 387
388 388
389 389 post_push_extension = ExtensionCallback(
390 390 hook_name='PUSH_HOOK',
391 391 kwargs_keys=(
392 392 'server_url', 'config', 'scm', 'username', 'ip', 'action',
393 393 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
394 394
395 395
396 396 pre_create_user = ExtensionCallback(
397 397 hook_name='PRE_CREATE_USER_HOOK',
398 398 kwargs_keys=(
399 399 'username', 'password', 'email', 'firstname', 'lastname', 'active',
400 400 'admin', 'created_by'))
401 401
402 402
403 log_create_pull_request = ExtensionCallback(
403 create_pull_request = ExtensionCallback(
404 404 hook_name='CREATE_PULL_REQUEST',
405 405 kwargs_keys=(
406 406 'server_url', 'config', 'scm', 'username', 'ip', 'action',
407 407 'repository', 'pull_request_id', 'url', 'title', 'description',
408 408 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
409 409 'mergeable', 'source', 'target', 'author', 'reviewers'))
410 410
411 411
412 log_merge_pull_request = ExtensionCallback(
412 merge_pull_request = ExtensionCallback(
413 413 hook_name='MERGE_PULL_REQUEST',
414 414 kwargs_keys=(
415 415 'server_url', 'config', 'scm', 'username', 'ip', 'action',
416 416 'repository', 'pull_request_id', 'url', 'title', 'description',
417 417 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
418 418 'mergeable', 'source', 'target', 'author', 'reviewers'))
419 419
420 420
421 log_close_pull_request = ExtensionCallback(
421 close_pull_request = ExtensionCallback(
422 422 hook_name='CLOSE_PULL_REQUEST',
423 423 kwargs_keys=(
424 424 'server_url', 'config', 'scm', 'username', 'ip', 'action',
425 425 'repository', 'pull_request_id', 'url', 'title', 'description',
426 426 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
427 427 'mergeable', 'source', 'target', 'author', 'reviewers'))
428 428
429 429
430 log_review_pull_request = ExtensionCallback(
430 review_pull_request = ExtensionCallback(
431 431 hook_name='REVIEW_PULL_REQUEST',
432 432 kwargs_keys=(
433 433 'server_url', 'config', 'scm', 'username', 'ip', 'action',
434 434 'repository', 'pull_request_id', 'url', 'title', 'description',
435 435 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
436 436 'mergeable', 'source', 'target', 'author', 'reviewers'))
437 437
438 438
439 log_comment_pull_request = ExtensionCallback(
439 comment_pull_request = ExtensionCallback(
440 440 hook_name='COMMENT_PULL_REQUEST',
441 441 kwargs_keys=(
442 442 'server_url', 'config', 'scm', 'username', 'ip', 'action',
443 443 'repository', 'pull_request_id', 'url', 'title', 'description',
444 444 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
445 445 'mergeable', 'source', 'target', 'author', 'reviewers'))
446 446
447 447
448 log_update_pull_request = ExtensionCallback(
448 comment_edit_pull_request = ExtensionCallback(
449 hook_name='COMMENT_EDIT_PULL_REQUEST',
450 kwargs_keys=(
451 'server_url', 'config', 'scm', 'username', 'ip', 'action',
452 'repository', 'pull_request_id', 'url', 'title', 'description',
453 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
454 'mergeable', 'source', 'target', 'author', 'reviewers'))
455
456
457 update_pull_request = ExtensionCallback(
449 458 hook_name='UPDATE_PULL_REQUEST',
450 459 kwargs_keys=(
451 460 'server_url', 'config', 'scm', 'username', 'ip', 'action',
452 461 'repository', 'pull_request_id', 'url', 'title', 'description',
453 462 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
454 463 'mergeable', 'source', 'target', 'author', 'reviewers'))
455 464
456 465
457 log_create_user = ExtensionCallback(
466 create_user = ExtensionCallback(
458 467 hook_name='CREATE_USER_HOOK',
459 468 kwargs_keys=(
460 469 'username', 'full_name_or_username', 'full_contact', 'user_id',
461 470 'name', 'firstname', 'short_contact', 'admin', 'lastname',
462 471 'ip_addresses', 'extern_type', 'extern_name',
463 472 'email', 'api_keys', 'last_login',
464 473 'full_name', 'active', 'password', 'emails',
465 474 'inherit_default_permissions', 'created_by', 'created_on'))
466 475
467 476
468 log_delete_user = ExtensionCallback(
477 delete_user = ExtensionCallback(
469 478 hook_name='DELETE_USER_HOOK',
470 479 kwargs_keys=(
471 480 'username', 'full_name_or_username', 'full_contact', 'user_id',
472 481 'name', 'firstname', 'short_contact', 'admin', 'lastname',
473 482 'ip_addresses',
474 483 'email', 'last_login',
475 484 'full_name', 'active', 'password', 'emails',
476 485 'inherit_default_permissions', 'deleted_by'))
477 486
478 487
479 log_create_repository = ExtensionCallback(
488 create_repository = ExtensionCallback(
480 489 hook_name='CREATE_REPO_HOOK',
481 490 kwargs_keys=(
482 491 'repo_name', 'repo_type', 'description', 'private', 'created_on',
483 492 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
484 493 'clone_uri', 'fork_id', 'group_id', 'created_by'))
485 494
486 495
487 log_delete_repository = ExtensionCallback(
496 delete_repository = ExtensionCallback(
488 497 hook_name='DELETE_REPO_HOOK',
489 498 kwargs_keys=(
490 499 'repo_name', 'repo_type', 'description', 'private', 'created_on',
491 500 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
492 501 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
493 502
494 503
495 log_comment_commit_repository = ExtensionCallback(
504 comment_commit_repository = ExtensionCallback(
496 505 hook_name='COMMENT_COMMIT_REPO_HOOK',
497 506 kwargs_keys=(
498 507 'repo_name', 'repo_type', 'description', 'private', 'created_on',
499 508 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
500 509 'clone_uri', 'fork_id', 'group_id',
501 510 'repository', 'created_by', 'comment', 'commit'))
502 511
512 comment_edit_commit_repository = ExtensionCallback(
513 hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
514 kwargs_keys=(
515 'repo_name', 'repo_type', 'description', 'private', 'created_on',
516 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
517 'clone_uri', 'fork_id', 'group_id',
518 'repository', 'created_by', 'comment', 'commit'))
503 519
504 log_create_repository_group = ExtensionCallback(
520
521 create_repository_group = ExtensionCallback(
505 522 hook_name='CREATE_REPO_GROUP_HOOK',
506 523 kwargs_keys=(
507 524 'group_name', 'group_parent_id', 'group_description',
508 525 'group_id', 'user_id', 'created_by', 'created_on',
509 526 'enable_locking'))
@@ -1,266 +1,264 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import webob
22 22 from pyramid.threadlocal import get_current_request
23 23
24 24 from rhodecode import events
25 25 from rhodecode.lib import hooks_base
26 26 from rhodecode.lib import utils2
27 27
28 28
29 29 def _supports_repo_type(repo_type):
30 30 if repo_type in ('hg', 'git'):
31 31 return True
32 32 return False
33 33
34 34
35 35 def _get_vcs_operation_context(username, repo_name, repo_type, action):
36 36 # NOTE(dan): import loop
37 37 from rhodecode.lib.base import vcs_operation_context
38 38
39 39 check_locking = action in ('pull', 'push')
40 40
41 41 request = get_current_request()
42 42
43 43 try:
44 44 environ = request.environ
45 45 except TypeError:
46 46 # we might use this outside of request context
47 47 environ = {}
48 48
49 49 if not environ:
50 50 environ = webob.Request.blank('').environ
51 51
52 52 extras = vcs_operation_context(environ, repo_name, username, action, repo_type, check_locking)
53 53 return utils2.AttributeDict(extras)
54 54
55 55
56 56 def trigger_post_push_hook(username, action, hook_type, repo_name, repo_type, commit_ids):
57 57 """
58 58 Triggers push action hooks
59 59
60 60 :param username: username who pushes
61 61 :param action: push/push_local/push_remote
62 62 :param hook_type: type of hook executed
63 63 :param repo_name: name of repo
64 64 :param repo_type: the type of SCM repo
65 65 :param commit_ids: list of commit ids that we pushed
66 66 """
67 67 extras = _get_vcs_operation_context(username, repo_name, repo_type, action)
68 68 extras.commit_ids = commit_ids
69 69 extras.hook_type = hook_type
70 70 hooks_base.post_push(extras)
71 71
72 72
73 73 def trigger_comment_commit_hooks(username, repo_name, repo_type, repo, data=None):
74 74 """
75 75 Triggers when a comment is made on a commit
76 76
77 77 :param username: username who creates the comment
78 78 :param repo_name: name of target repo
79 79 :param repo_type: the type of SCM target repo
80 80 :param repo: the repo object we trigger the event for
81 81 :param data: extra data for specific events e.g {'comment': comment_obj, 'commit': commit_obj}
82 82 """
83 83 if not _supports_repo_type(repo_type):
84 84 return
85 85
86 86 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_commit')
87 87
88 88 comment = data['comment']
89 89 commit = data['commit']
90 90
91 91 events.trigger(events.RepoCommitCommentEvent(repo, commit, comment))
92 92 extras.update(repo.get_dict())
93 93
94 94 extras.commit = commit.serialize()
95 95 extras.comment = comment.get_api_data()
96 96 extras.created_by = username
97 hooks_base.log_comment_commit_repository(**extras)
97 hooks_base.comment_commit_repository(**extras)
98 98
99 99
100 100 def trigger_comment_commit_edit_hooks(username, repo_name, repo_type, repo, data=None):
101 101 """
102 102 Triggers when a comment is edited on a commit
103 103
104 104 :param username: username who edits the comment
105 105 :param repo_name: name of target repo
106 106 :param repo_type: the type of SCM target repo
107 107 :param repo: the repo object we trigger the event for
108 108 :param data: extra data for specific events e.g {'comment': comment_obj, 'commit': commit_obj}
109 109 """
110 110 if not _supports_repo_type(repo_type):
111 111 return
112 112
113 113 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_commit')
114 114
115 115 comment = data['comment']
116 116 commit = data['commit']
117 117
118 118 events.trigger(events.RepoCommitCommentEditEvent(repo, commit, comment))
119 119 extras.update(repo.get_dict())
120 120
121 121 extras.commit = commit.serialize()
122 122 extras.comment = comment.get_api_data()
123 123 extras.created_by = username
124 # TODO(marcink): rcextensions handlers ??
125 hooks_base.log_comment_commit_repository(**extras)
124 hooks_base.comment_edit_commit_repository(**extras)
126 125
127 126
128 127 def trigger_create_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
129 128 """
130 129 Triggers create pull request action hooks
131 130
132 131 :param username: username who creates the pull request
133 132 :param repo_name: name of target repo
134 133 :param repo_type: the type of SCM target repo
135 134 :param pull_request: the pull request that was created
136 135 :param data: extra data for specific events e.g {'comment': comment_obj}
137 136 """
138 137 if not _supports_repo_type(repo_type):
139 138 return
140 139
141 140 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'create_pull_request')
142 141 events.trigger(events.PullRequestCreateEvent(pull_request))
143 142 extras.update(pull_request.get_api_data(with_merge_state=False))
144 hooks_base.log_create_pull_request(**extras)
143 hooks_base.create_pull_request(**extras)
145 144
146 145
147 146 def trigger_merge_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
148 147 """
149 148 Triggers merge pull request action hooks
150 149
151 150 :param username: username who creates the pull request
152 151 :param repo_name: name of target repo
153 152 :param repo_type: the type of SCM target repo
154 153 :param pull_request: the pull request that was merged
155 154 :param data: extra data for specific events e.g {'comment': comment_obj}
156 155 """
157 156 if not _supports_repo_type(repo_type):
158 157 return
159 158
160 159 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'merge_pull_request')
161 160 events.trigger(events.PullRequestMergeEvent(pull_request))
162 161 extras.update(pull_request.get_api_data())
163 hooks_base.log_merge_pull_request(**extras)
162 hooks_base.merge_pull_request(**extras)
164 163
165 164
166 165 def trigger_close_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
167 166 """
168 167 Triggers close pull request action hooks
169 168
170 169 :param username: username who creates the pull request
171 170 :param repo_name: name of target repo
172 171 :param repo_type: the type of SCM target repo
173 172 :param pull_request: the pull request that was closed
174 173 :param data: extra data for specific events e.g {'comment': comment_obj}
175 174 """
176 175 if not _supports_repo_type(repo_type):
177 176 return
178 177
179 178 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'close_pull_request')
180 179 events.trigger(events.PullRequestCloseEvent(pull_request))
181 180 extras.update(pull_request.get_api_data())
182 hooks_base.log_close_pull_request(**extras)
181 hooks_base.close_pull_request(**extras)
183 182
184 183
185 184 def trigger_review_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
186 185 """
187 186 Triggers review status change pull request action hooks
188 187
189 188 :param username: username who creates the pull request
190 189 :param repo_name: name of target repo
191 190 :param repo_type: the type of SCM target repo
192 191 :param pull_request: the pull request that review status changed
193 192 :param data: extra data for specific events e.g {'comment': comment_obj}
194 193 """
195 194 if not _supports_repo_type(repo_type):
196 195 return
197 196
198 197 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'review_pull_request')
199 198 status = data.get('status')
200 199 events.trigger(events.PullRequestReviewEvent(pull_request, status))
201 200 extras.update(pull_request.get_api_data())
202 hooks_base.log_review_pull_request(**extras)
201 hooks_base.review_pull_request(**extras)
203 202
204 203
205 204 def trigger_comment_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
206 205 """
207 206 Triggers when a comment is made on a pull request
208 207
209 208 :param username: username who creates the pull request
210 209 :param repo_name: name of target repo
211 210 :param repo_type: the type of SCM target repo
212 211 :param pull_request: the pull request that comment was made on
213 212 :param data: extra data for specific events e.g {'comment': comment_obj}
214 213 """
215 214 if not _supports_repo_type(repo_type):
216 215 return
217 216
218 217 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_pull_request')
219 218
220 219 comment = data['comment']
221 220 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
222 221 extras.update(pull_request.get_api_data())
223 222 extras.comment = comment.get_api_data()
224 hooks_base.log_comment_pull_request(**extras)
223 hooks_base.comment_pull_request(**extras)
225 224
226 225
227 226 def trigger_comment_pull_request_edit_hook(username, repo_name, repo_type, pull_request, data=None):
228 227 """
229 228 Triggers when a comment was edited on a pull request
230 229
231 230 :param username: username who made the edit
232 231 :param repo_name: name of target repo
233 232 :param repo_type: the type of SCM target repo
234 233 :param pull_request: the pull request that comment was made on
235 234 :param data: extra data for specific events e.g {'comment': comment_obj}
236 235 """
237 236 if not _supports_repo_type(repo_type):
238 237 return
239 238
240 239 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_pull_request')
241 240
242 241 comment = data['comment']
243 242 events.trigger(events.PullRequestCommentEditEvent(pull_request, comment))
244 243 extras.update(pull_request.get_api_data())
245 244 extras.comment = comment.get_api_data()
246 # TODO(marcink): handle rcextensions...
247 hooks_base.log_comment_pull_request(**extras)
245 hooks_base.comment_edit_pull_request(**extras)
248 246
249 247
250 248 def trigger_update_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
251 249 """
252 250 Triggers update pull request action hooks
253 251
254 252 :param username: username who creates the pull request
255 253 :param repo_name: name of target repo
256 254 :param repo_type: the type of SCM target repo
257 255 :param pull_request: the pull request that was updated
258 256 :param data: extra data for specific events e.g {'comment': comment_obj}
259 257 """
260 258 if not _supports_repo_type(repo_type):
261 259 return
262 260
263 261 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'update_pull_request')
264 262 events.trigger(events.PullRequestUpdateEvent(pull_request))
265 263 extras.update(pull_request.get_api_data())
266 hooks_base.log_update_pull_request(**extras)
264 hooks_base.update_pull_request(**extras)
@@ -1,1172 +1,1172 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import shutil
24 24 import time
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28
29 29 from pyramid.threadlocal import get_current_request
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode import events
33 33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 34 from rhodecode.lib.caching_query import FromCache
35 35 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
36 from rhodecode.lib.hooks_base import log_delete_repository
36 from rhodecode.lib import hooks_base
37 37 from rhodecode.lib.user_log_filter import user_log_filter
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
40 40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
41 41 get_current_rhodecode_user, safe_int, action_logger_generic)
42 42 from rhodecode.lib.vcs.backends import get_backend
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, func, case, joinedload, or_, in_filter_generator,
46 46 Session, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49 49 from rhodecode.model.settings import VcsSettingsModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class RepoModel(BaseModel):
55 55
56 56 cls = Repository
57 57
58 58 def _get_user_group(self, users_group):
59 59 return self._get_instance(UserGroup, users_group,
60 60 callback=UserGroup.get_by_group_name)
61 61
62 62 def _get_repo_group(self, repo_group):
63 63 return self._get_instance(RepoGroup, repo_group,
64 64 callback=RepoGroup.get_by_group_name)
65 65
66 66 def _create_default_perms(self, repository, private):
67 67 # create default permission
68 68 default = 'repository.read'
69 69 def_user = User.get_default_user()
70 70 for p in def_user.user_perms:
71 71 if p.permission.permission_name.startswith('repository.'):
72 72 default = p.permission.permission_name
73 73 break
74 74
75 75 default_perm = 'repository.none' if private else default
76 76
77 77 repo_to_perm = UserRepoToPerm()
78 78 repo_to_perm.permission = Permission.get_by_key(default_perm)
79 79
80 80 repo_to_perm.repository = repository
81 81 repo_to_perm.user_id = def_user.user_id
82 82
83 83 return repo_to_perm
84 84
85 85 @LazyProperty
86 86 def repos_path(self):
87 87 """
88 88 Gets the repositories root path from database
89 89 """
90 90 settings_model = VcsSettingsModel(sa=self.sa)
91 91 return settings_model.get_repos_location()
92 92
93 93 def get(self, repo_id):
94 94 repo = self.sa.query(Repository) \
95 95 .filter(Repository.repo_id == repo_id)
96 96
97 97 return repo.scalar()
98 98
99 99 def get_repo(self, repository):
100 100 return self._get_repo(repository)
101 101
102 102 def get_by_repo_name(self, repo_name, cache=False):
103 103 repo = self.sa.query(Repository) \
104 104 .filter(Repository.repo_name == repo_name)
105 105
106 106 if cache:
107 107 name_key = _hash_key(repo_name)
108 108 repo = repo.options(
109 109 FromCache("sql_cache_short", "get_repo_%s" % name_key))
110 110 return repo.scalar()
111 111
112 112 def _extract_id_from_repo_name(self, repo_name):
113 113 if repo_name.startswith('/'):
114 114 repo_name = repo_name.lstrip('/')
115 115 by_id_match = re.match(r'^_(\d{1,})', repo_name)
116 116 if by_id_match:
117 117 return by_id_match.groups()[0]
118 118
119 119 def get_repo_by_id(self, repo_name):
120 120 """
121 121 Extracts repo_name by id from special urls.
122 122 Example url is _11/repo_name
123 123
124 124 :param repo_name:
125 125 :return: repo object if matched else None
126 126 """
127 127
128 128 try:
129 129 _repo_id = self._extract_id_from_repo_name(repo_name)
130 130 if _repo_id:
131 131 return self.get(_repo_id)
132 132 except Exception:
133 133 log.exception('Failed to extract repo_name from URL')
134 134
135 135 return None
136 136
137 137 def get_repos_for_root(self, root, traverse=False):
138 138 if traverse:
139 139 like_expression = u'{}%'.format(safe_unicode(root))
140 140 repos = Repository.query().filter(
141 141 Repository.repo_name.like(like_expression)).all()
142 142 else:
143 143 if root and not isinstance(root, RepoGroup):
144 144 raise ValueError(
145 145 'Root must be an instance '
146 146 'of RepoGroup, got:{} instead'.format(type(root)))
147 147 repos = Repository.query().filter(Repository.group == root).all()
148 148 return repos
149 149
150 150 def get_url(self, repo, request=None, permalink=False):
151 151 if not request:
152 152 request = get_current_request()
153 153
154 154 if not request:
155 155 return
156 156
157 157 if permalink:
158 158 return request.route_url(
159 159 'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
160 160 else:
161 161 return request.route_url(
162 162 'repo_summary', repo_name=safe_str(repo.repo_name))
163 163
164 164 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
165 165 if not request:
166 166 request = get_current_request()
167 167
168 168 if not request:
169 169 return
170 170
171 171 if permalink:
172 172 return request.route_url(
173 173 'repo_commit', repo_name=safe_str(repo.repo_id),
174 174 commit_id=commit_id)
175 175
176 176 else:
177 177 return request.route_url(
178 178 'repo_commit', repo_name=safe_str(repo.repo_name),
179 179 commit_id=commit_id)
180 180
181 181 def get_repo_log(self, repo, filter_term):
182 182 repo_log = UserLog.query()\
183 183 .filter(or_(UserLog.repository_id == repo.repo_id,
184 184 UserLog.repository_name == repo.repo_name))\
185 185 .options(joinedload(UserLog.user))\
186 186 .options(joinedload(UserLog.repository))\
187 187 .order_by(UserLog.action_date.desc())
188 188
189 189 repo_log = user_log_filter(repo_log, filter_term)
190 190 return repo_log
191 191
192 192 @classmethod
193 193 def update_commit_cache(cls, repositories=None):
194 194 if not repositories:
195 195 repositories = Repository.getAll()
196 196 for repo in repositories:
197 197 repo.update_commit_cache()
198 198
199 199 def get_repos_as_dict(self, repo_list=None, admin=False,
200 200 super_user_actions=False, short_name=None):
201 201
202 202 _render = get_current_request().get_partial_renderer(
203 203 'rhodecode:templates/data_table/_dt_elements.mako')
204 204 c = _render.get_call_context()
205 205 h = _render.get_helpers()
206 206
207 207 def quick_menu(repo_name):
208 208 return _render('quick_menu', repo_name)
209 209
210 210 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
211 211 if short_name is not None:
212 212 short_name_var = short_name
213 213 else:
214 214 short_name_var = not admin
215 215 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
216 216 short_name=short_name_var, admin=False)
217 217
218 218 def last_change(last_change):
219 219 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
220 220 ts = time.time()
221 221 utc_offset = (datetime.datetime.fromtimestamp(ts)
222 222 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
223 223 last_change = last_change + datetime.timedelta(seconds=utc_offset)
224 224
225 225 return _render("last_change", last_change)
226 226
227 227 def rss_lnk(repo_name):
228 228 return _render("rss", repo_name)
229 229
230 230 def atom_lnk(repo_name):
231 231 return _render("atom", repo_name)
232 232
233 233 def last_rev(repo_name, cs_cache):
234 234 return _render('revision', repo_name, cs_cache.get('revision'),
235 235 cs_cache.get('raw_id'), cs_cache.get('author'),
236 236 cs_cache.get('message'), cs_cache.get('date'))
237 237
238 238 def desc(desc):
239 239 return _render('repo_desc', desc, c.visual.stylify_metatags)
240 240
241 241 def state(repo_state):
242 242 return _render("repo_state", repo_state)
243 243
244 244 def repo_actions(repo_name):
245 245 return _render('repo_actions', repo_name, super_user_actions)
246 246
247 247 def user_profile(username):
248 248 return _render('user_profile', username)
249 249
250 250 repos_data = []
251 251 for repo in repo_list:
252 252 # NOTE(marcink): because we use only raw column we need to load it like that
253 253 changeset_cache = Repository._load_changeset_cache(
254 254 repo.repo_id, repo._changeset_cache)
255 255
256 256 row = {
257 257 "menu": quick_menu(repo.repo_name),
258 258
259 259 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
260 260 repo.private, repo.archived, repo.fork),
261 261
262 262 "desc": desc(h.escape(repo.description)),
263 263
264 264 "last_change": last_change(repo.updated_on),
265 265
266 266 "last_changeset": last_rev(repo.repo_name, changeset_cache),
267 267 "last_changeset_raw": changeset_cache.get('revision'),
268 268
269 269 "owner": user_profile(repo.User.username),
270 270
271 271 "state": state(repo.repo_state),
272 272 "rss": rss_lnk(repo.repo_name),
273 273 "atom": atom_lnk(repo.repo_name),
274 274 }
275 275 if admin:
276 276 row.update({
277 277 "action": repo_actions(repo.repo_name),
278 278 })
279 279 repos_data.append(row)
280 280
281 281 return repos_data
282 282
283 283 def get_repos_data_table(
284 284 self, draw, start, limit,
285 285 search_q, order_by, order_dir,
286 286 auth_user, repo_group_id):
287 287 from rhodecode.model.scm import RepoList
288 288
289 289 _perms = ['repository.read', 'repository.write', 'repository.admin']
290 290
291 291 repos = Repository.query() \
292 292 .filter(Repository.group_id == repo_group_id) \
293 293 .all()
294 294 auth_repo_list = RepoList(
295 295 repos, perm_set=_perms,
296 296 extra_kwargs=dict(user=auth_user))
297 297
298 298 allowed_ids = [-1]
299 299 for repo in auth_repo_list:
300 300 allowed_ids.append(repo.repo_id)
301 301
302 302 repos_data_total_count = Repository.query() \
303 303 .filter(Repository.group_id == repo_group_id) \
304 304 .filter(or_(
305 305 # generate multiple IN to fix limitation problems
306 306 *in_filter_generator(Repository.repo_id, allowed_ids))
307 307 ) \
308 308 .count()
309 309
310 310 base_q = Session.query(
311 311 Repository.repo_id,
312 312 Repository.repo_name,
313 313 Repository.description,
314 314 Repository.repo_type,
315 315 Repository.repo_state,
316 316 Repository.private,
317 317 Repository.archived,
318 318 Repository.fork,
319 319 Repository.updated_on,
320 320 Repository._changeset_cache,
321 321 User,
322 322 ) \
323 323 .filter(Repository.group_id == repo_group_id) \
324 324 .filter(or_(
325 325 # generate multiple IN to fix limitation problems
326 326 *in_filter_generator(Repository.repo_id, allowed_ids))
327 327 ) \
328 328 .join(User, User.user_id == Repository.user_id) \
329 329 .group_by(Repository, User)
330 330
331 331 repos_data_total_filtered_count = base_q.count()
332 332
333 333 sort_defined = False
334 334 if order_by == 'repo_name':
335 335 sort_col = func.lower(Repository.repo_name)
336 336 sort_defined = True
337 337 elif order_by == 'user_username':
338 338 sort_col = User.username
339 339 else:
340 340 sort_col = getattr(Repository, order_by, None)
341 341
342 342 if sort_defined or sort_col:
343 343 if order_dir == 'asc':
344 344 sort_col = sort_col.asc()
345 345 else:
346 346 sort_col = sort_col.desc()
347 347
348 348 base_q = base_q.order_by(sort_col)
349 349 base_q = base_q.offset(start).limit(limit)
350 350
351 351 repos_list = base_q.all()
352 352
353 353 repos_data = RepoModel().get_repos_as_dict(
354 354 repo_list=repos_list, admin=False)
355 355
356 356 data = ({
357 357 'draw': draw,
358 358 'data': repos_data,
359 359 'recordsTotal': repos_data_total_count,
360 360 'recordsFiltered': repos_data_total_filtered_count,
361 361 })
362 362 return data
363 363
364 364 def _get_defaults(self, repo_name):
365 365 """
366 366 Gets information about repository, and returns a dict for
367 367 usage in forms
368 368
369 369 :param repo_name:
370 370 """
371 371
372 372 repo_info = Repository.get_by_repo_name(repo_name)
373 373
374 374 if repo_info is None:
375 375 return None
376 376
377 377 defaults = repo_info.get_dict()
378 378 defaults['repo_name'] = repo_info.just_name
379 379
380 380 groups = repo_info.groups_with_parents
381 381 parent_group = groups[-1] if groups else None
382 382
383 383 # we use -1 as this is how in HTML, we mark an empty group
384 384 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
385 385
386 386 keys_to_process = (
387 387 {'k': 'repo_type', 'strip': False},
388 388 {'k': 'repo_enable_downloads', 'strip': True},
389 389 {'k': 'repo_description', 'strip': True},
390 390 {'k': 'repo_enable_locking', 'strip': True},
391 391 {'k': 'repo_landing_rev', 'strip': True},
392 392 {'k': 'clone_uri', 'strip': False},
393 393 {'k': 'push_uri', 'strip': False},
394 394 {'k': 'repo_private', 'strip': True},
395 395 {'k': 'repo_enable_statistics', 'strip': True}
396 396 )
397 397
398 398 for item in keys_to_process:
399 399 attr = item['k']
400 400 if item['strip']:
401 401 attr = remove_prefix(item['k'], 'repo_')
402 402
403 403 val = defaults[attr]
404 404 if item['k'] == 'repo_landing_rev':
405 405 val = ':'.join(defaults[attr])
406 406 defaults[item['k']] = val
407 407 if item['k'] == 'clone_uri':
408 408 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
409 409 if item['k'] == 'push_uri':
410 410 defaults['push_uri_hidden'] = repo_info.push_uri_hidden
411 411
412 412 # fill owner
413 413 if repo_info.user:
414 414 defaults.update({'user': repo_info.user.username})
415 415 else:
416 416 replacement_user = User.get_first_super_admin().username
417 417 defaults.update({'user': replacement_user})
418 418
419 419 return defaults
420 420
421 421 def update(self, repo, **kwargs):
422 422 try:
423 423 cur_repo = self._get_repo(repo)
424 424 source_repo_name = cur_repo.repo_name
425 425 if 'user' in kwargs:
426 426 cur_repo.user = User.get_by_username(kwargs['user'])
427 427
428 428 if 'repo_group' in kwargs:
429 429 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
430 430 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
431 431
432 432 update_keys = [
433 433 (1, 'repo_description'),
434 434 (1, 'repo_landing_rev'),
435 435 (1, 'repo_private'),
436 436 (1, 'repo_enable_downloads'),
437 437 (1, 'repo_enable_locking'),
438 438 (1, 'repo_enable_statistics'),
439 439 (0, 'clone_uri'),
440 440 (0, 'push_uri'),
441 441 (0, 'fork_id')
442 442 ]
443 443 for strip, k in update_keys:
444 444 if k in kwargs:
445 445 val = kwargs[k]
446 446 if strip:
447 447 k = remove_prefix(k, 'repo_')
448 448
449 449 setattr(cur_repo, k, val)
450 450
451 451 new_name = cur_repo.get_new_name(kwargs['repo_name'])
452 452 cur_repo.repo_name = new_name
453 453
454 454 # if private flag is set, reset default permission to NONE
455 455 if kwargs.get('repo_private'):
456 456 EMPTY_PERM = 'repository.none'
457 457 RepoModel().grant_user_permission(
458 458 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
459 459 )
460 460
461 461 # handle extra fields
462 462 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
463 463 k = RepositoryField.un_prefix_key(field)
464 464 ex_field = RepositoryField.get_by_key_name(
465 465 key=k, repo=cur_repo)
466 466 if ex_field:
467 467 ex_field.field_value = kwargs[field]
468 468 self.sa.add(ex_field)
469 469
470 470 self.sa.add(cur_repo)
471 471
472 472 if source_repo_name != new_name:
473 473 # rename repository
474 474 self._rename_filesystem_repo(
475 475 old=source_repo_name, new=new_name)
476 476
477 477 return cur_repo
478 478 except Exception:
479 479 log.error(traceback.format_exc())
480 480 raise
481 481
482 482 def _create_repo(self, repo_name, repo_type, description, owner,
483 483 private=False, clone_uri=None, repo_group=None,
484 484 landing_rev='rev:tip', fork_of=None,
485 485 copy_fork_permissions=False, enable_statistics=False,
486 486 enable_locking=False, enable_downloads=False,
487 487 copy_group_permissions=False,
488 488 state=Repository.STATE_PENDING):
489 489 """
490 490 Create repository inside database with PENDING state, this should be
491 491 only executed by create() repo. With exception of importing existing
492 492 repos
493 493 """
494 494 from rhodecode.model.scm import ScmModel
495 495
496 496 owner = self._get_user(owner)
497 497 fork_of = self._get_repo(fork_of)
498 498 repo_group = self._get_repo_group(safe_int(repo_group))
499 499
500 500 try:
501 501 repo_name = safe_unicode(repo_name)
502 502 description = safe_unicode(description)
503 503 # repo name is just a name of repository
504 504 # while repo_name_full is a full qualified name that is combined
505 505 # with name and path of group
506 506 repo_name_full = repo_name
507 507 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
508 508
509 509 new_repo = Repository()
510 510 new_repo.repo_state = state
511 511 new_repo.enable_statistics = False
512 512 new_repo.repo_name = repo_name_full
513 513 new_repo.repo_type = repo_type
514 514 new_repo.user = owner
515 515 new_repo.group = repo_group
516 516 new_repo.description = description or repo_name
517 517 new_repo.private = private
518 518 new_repo.archived = False
519 519 new_repo.clone_uri = clone_uri
520 520 new_repo.landing_rev = landing_rev
521 521
522 522 new_repo.enable_statistics = enable_statistics
523 523 new_repo.enable_locking = enable_locking
524 524 new_repo.enable_downloads = enable_downloads
525 525
526 526 if repo_group:
527 527 new_repo.enable_locking = repo_group.enable_locking
528 528
529 529 if fork_of:
530 530 parent_repo = fork_of
531 531 new_repo.fork = parent_repo
532 532
533 533 events.trigger(events.RepoPreCreateEvent(new_repo))
534 534
535 535 self.sa.add(new_repo)
536 536
537 537 EMPTY_PERM = 'repository.none'
538 538 if fork_of and copy_fork_permissions:
539 539 repo = fork_of
540 540 user_perms = UserRepoToPerm.query() \
541 541 .filter(UserRepoToPerm.repository == repo).all()
542 542 group_perms = UserGroupRepoToPerm.query() \
543 543 .filter(UserGroupRepoToPerm.repository == repo).all()
544 544
545 545 for perm in user_perms:
546 546 UserRepoToPerm.create(
547 547 perm.user, new_repo, perm.permission)
548 548
549 549 for perm in group_perms:
550 550 UserGroupRepoToPerm.create(
551 551 perm.users_group, new_repo, perm.permission)
552 552 # in case we copy permissions and also set this repo to private
553 553 # override the default user permission to make it a private repo
554 554 if private:
555 555 RepoModel(self.sa).grant_user_permission(
556 556 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
557 557
558 558 elif repo_group and copy_group_permissions:
559 559 user_perms = UserRepoGroupToPerm.query() \
560 560 .filter(UserRepoGroupToPerm.group == repo_group).all()
561 561
562 562 group_perms = UserGroupRepoGroupToPerm.query() \
563 563 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
564 564
565 565 for perm in user_perms:
566 566 perm_name = perm.permission.permission_name.replace(
567 567 'group.', 'repository.')
568 568 perm_obj = Permission.get_by_key(perm_name)
569 569 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
570 570
571 571 for perm in group_perms:
572 572 perm_name = perm.permission.permission_name.replace(
573 573 'group.', 'repository.')
574 574 perm_obj = Permission.get_by_key(perm_name)
575 575 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
576 576
577 577 if private:
578 578 RepoModel(self.sa).grant_user_permission(
579 579 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
580 580
581 581 else:
582 582 perm_obj = self._create_default_perms(new_repo, private)
583 583 self.sa.add(perm_obj)
584 584
585 585 # now automatically start following this repository as owner
586 586 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
587 587
588 588 # we need to flush here, in order to check if database won't
589 589 # throw any exceptions, create filesystem dirs at the very end
590 590 self.sa.flush()
591 591 events.trigger(events.RepoCreateEvent(new_repo))
592 592 return new_repo
593 593
594 594 except Exception:
595 595 log.error(traceback.format_exc())
596 596 raise
597 597
598 598 def create(self, form_data, cur_user):
599 599 """
600 600 Create repository using celery tasks
601 601
602 602 :param form_data:
603 603 :param cur_user:
604 604 """
605 605 from rhodecode.lib.celerylib import tasks, run_task
606 606 return run_task(tasks.create_repo, form_data, cur_user)
607 607
608 608 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
609 609 perm_deletions=None, check_perms=True,
610 610 cur_user=None):
611 611 if not perm_additions:
612 612 perm_additions = []
613 613 if not perm_updates:
614 614 perm_updates = []
615 615 if not perm_deletions:
616 616 perm_deletions = []
617 617
618 618 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
619 619
620 620 changes = {
621 621 'added': [],
622 622 'updated': [],
623 623 'deleted': [],
624 624 'default_user_changed': None
625 625 }
626 626
627 627 repo = self._get_repo(repo)
628 628
629 629 # update permissions
630 630 for member_id, perm, member_type in perm_updates:
631 631 member_id = int(member_id)
632 632 if member_type == 'user':
633 633 member_name = User.get(member_id).username
634 634 if member_name == User.DEFAULT_USER:
635 635 # NOTE(dan): detect if we changed permissions for default user
636 636 perm_obj = self.sa.query(UserRepoToPerm) \
637 637 .filter(UserRepoToPerm.user_id == member_id) \
638 638 .filter(UserRepoToPerm.repository == repo) \
639 639 .scalar()
640 640 if perm_obj and perm_obj.permission.permission_name != perm:
641 641 changes['default_user_changed'] = True
642 642
643 643 # this updates also current one if found
644 644 self.grant_user_permission(
645 645 repo=repo, user=member_id, perm=perm)
646 646 elif member_type == 'user_group':
647 647 # check if we have permissions to alter this usergroup
648 648 member_name = UserGroup.get(member_id).users_group_name
649 649 if not check_perms or HasUserGroupPermissionAny(
650 650 *req_perms)(member_name, user=cur_user):
651 651 self.grant_user_group_permission(
652 652 repo=repo, group_name=member_id, perm=perm)
653 653 else:
654 654 raise ValueError("member_type must be 'user' or 'user_group' "
655 655 "got {} instead".format(member_type))
656 656 changes['updated'].append({'type': member_type, 'id': member_id,
657 657 'name': member_name, 'new_perm': perm})
658 658
659 659 # set new permissions
660 660 for member_id, perm, member_type in perm_additions:
661 661 member_id = int(member_id)
662 662 if member_type == 'user':
663 663 member_name = User.get(member_id).username
664 664 self.grant_user_permission(
665 665 repo=repo, user=member_id, perm=perm)
666 666 elif member_type == 'user_group':
667 667 # check if we have permissions to alter this usergroup
668 668 member_name = UserGroup.get(member_id).users_group_name
669 669 if not check_perms or HasUserGroupPermissionAny(
670 670 *req_perms)(member_name, user=cur_user):
671 671 self.grant_user_group_permission(
672 672 repo=repo, group_name=member_id, perm=perm)
673 673 else:
674 674 raise ValueError("member_type must be 'user' or 'user_group' "
675 675 "got {} instead".format(member_type))
676 676
677 677 changes['added'].append({'type': member_type, 'id': member_id,
678 678 'name': member_name, 'new_perm': perm})
679 679 # delete permissions
680 680 for member_id, perm, member_type in perm_deletions:
681 681 member_id = int(member_id)
682 682 if member_type == 'user':
683 683 member_name = User.get(member_id).username
684 684 self.revoke_user_permission(repo=repo, user=member_id)
685 685 elif member_type == 'user_group':
686 686 # check if we have permissions to alter this usergroup
687 687 member_name = UserGroup.get(member_id).users_group_name
688 688 if not check_perms or HasUserGroupPermissionAny(
689 689 *req_perms)(member_name, user=cur_user):
690 690 self.revoke_user_group_permission(
691 691 repo=repo, group_name=member_id)
692 692 else:
693 693 raise ValueError("member_type must be 'user' or 'user_group' "
694 694 "got {} instead".format(member_type))
695 695
696 696 changes['deleted'].append({'type': member_type, 'id': member_id,
697 697 'name': member_name, 'new_perm': perm})
698 698 return changes
699 699
700 700 def create_fork(self, form_data, cur_user):
701 701 """
702 702 Simple wrapper into executing celery task for fork creation
703 703
704 704 :param form_data:
705 705 :param cur_user:
706 706 """
707 707 from rhodecode.lib.celerylib import tasks, run_task
708 708 return run_task(tasks.create_repo_fork, form_data, cur_user)
709 709
710 710 def archive(self, repo):
711 711 """
712 712 Archive given repository. Set archive flag.
713 713
714 714 :param repo:
715 715 """
716 716 repo = self._get_repo(repo)
717 717 if repo:
718 718
719 719 try:
720 720 repo.archived = True
721 721 self.sa.add(repo)
722 722 self.sa.commit()
723 723 except Exception:
724 724 log.error(traceback.format_exc())
725 725 raise
726 726
727 727 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
728 728 """
729 729 Delete given repository, forks parameter defines what do do with
730 730 attached forks. Throws AttachedForksError if deleted repo has attached
731 731 forks
732 732
733 733 :param repo:
734 734 :param forks: str 'delete' or 'detach'
735 735 :param pull_requests: str 'delete' or None
736 736 :param fs_remove: remove(archive) repo from filesystem
737 737 """
738 738 if not cur_user:
739 739 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
740 740 repo = self._get_repo(repo)
741 741 if repo:
742 742 if forks == 'detach':
743 743 for r in repo.forks:
744 744 r.fork = None
745 745 self.sa.add(r)
746 746 elif forks == 'delete':
747 747 for r in repo.forks:
748 748 self.delete(r, forks='delete')
749 749 elif [f for f in repo.forks]:
750 750 raise AttachedForksError()
751 751
752 752 # check for pull requests
753 753 pr_sources = repo.pull_requests_source
754 754 pr_targets = repo.pull_requests_target
755 755 if pull_requests != 'delete' and (pr_sources or pr_targets):
756 756 raise AttachedPullRequestsError()
757 757
758 758 old_repo_dict = repo.get_dict()
759 759 events.trigger(events.RepoPreDeleteEvent(repo))
760 760 try:
761 761 self.sa.delete(repo)
762 762 if fs_remove:
763 763 self._delete_filesystem_repo(repo)
764 764 else:
765 765 log.debug('skipping removal from filesystem')
766 766 old_repo_dict.update({
767 767 'deleted_by': cur_user,
768 768 'deleted_on': time.time(),
769 769 })
770 log_delete_repository(**old_repo_dict)
770 hooks_base.delete_repository(**old_repo_dict)
771 771 events.trigger(events.RepoDeleteEvent(repo))
772 772 except Exception:
773 773 log.error(traceback.format_exc())
774 774 raise
775 775
776 776 def grant_user_permission(self, repo, user, perm):
777 777 """
778 778 Grant permission for user on given repository, or update existing one
779 779 if found
780 780
781 781 :param repo: Instance of Repository, repository_id, or repository name
782 782 :param user: Instance of User, user_id or username
783 783 :param perm: Instance of Permission, or permission_name
784 784 """
785 785 user = self._get_user(user)
786 786 repo = self._get_repo(repo)
787 787 permission = self._get_perm(perm)
788 788
789 789 # check if we have that permission already
790 790 obj = self.sa.query(UserRepoToPerm) \
791 791 .filter(UserRepoToPerm.user == user) \
792 792 .filter(UserRepoToPerm.repository == repo) \
793 793 .scalar()
794 794 if obj is None:
795 795 # create new !
796 796 obj = UserRepoToPerm()
797 797 obj.repository = repo
798 798 obj.user = user
799 799 obj.permission = permission
800 800 self.sa.add(obj)
801 801 log.debug('Granted perm %s to %s on %s', perm, user, repo)
802 802 action_logger_generic(
803 803 'granted permission: {} to user: {} on repo: {}'.format(
804 804 perm, user, repo), namespace='security.repo')
805 805 return obj
806 806
807 807 def revoke_user_permission(self, repo, user):
808 808 """
809 809 Revoke permission for user on given repository
810 810
811 811 :param repo: Instance of Repository, repository_id, or repository name
812 812 :param user: Instance of User, user_id or username
813 813 """
814 814
815 815 user = self._get_user(user)
816 816 repo = self._get_repo(repo)
817 817
818 818 obj = self.sa.query(UserRepoToPerm) \
819 819 .filter(UserRepoToPerm.repository == repo) \
820 820 .filter(UserRepoToPerm.user == user) \
821 821 .scalar()
822 822 if obj:
823 823 self.sa.delete(obj)
824 824 log.debug('Revoked perm on %s on %s', repo, user)
825 825 action_logger_generic(
826 826 'revoked permission from user: {} on repo: {}'.format(
827 827 user, repo), namespace='security.repo')
828 828
829 829 def grant_user_group_permission(self, repo, group_name, perm):
830 830 """
831 831 Grant permission for user group on given repository, or update
832 832 existing one if found
833 833
834 834 :param repo: Instance of Repository, repository_id, or repository name
835 835 :param group_name: Instance of UserGroup, users_group_id,
836 836 or user group name
837 837 :param perm: Instance of Permission, or permission_name
838 838 """
839 839 repo = self._get_repo(repo)
840 840 group_name = self._get_user_group(group_name)
841 841 permission = self._get_perm(perm)
842 842
843 843 # check if we have that permission already
844 844 obj = self.sa.query(UserGroupRepoToPerm) \
845 845 .filter(UserGroupRepoToPerm.users_group == group_name) \
846 846 .filter(UserGroupRepoToPerm.repository == repo) \
847 847 .scalar()
848 848
849 849 if obj is None:
850 850 # create new
851 851 obj = UserGroupRepoToPerm()
852 852
853 853 obj.repository = repo
854 854 obj.users_group = group_name
855 855 obj.permission = permission
856 856 self.sa.add(obj)
857 857 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
858 858 action_logger_generic(
859 859 'granted permission: {} to usergroup: {} on repo: {}'.format(
860 860 perm, group_name, repo), namespace='security.repo')
861 861
862 862 return obj
863 863
864 864 def revoke_user_group_permission(self, repo, group_name):
865 865 """
866 866 Revoke permission for user group on given repository
867 867
868 868 :param repo: Instance of Repository, repository_id, or repository name
869 869 :param group_name: Instance of UserGroup, users_group_id,
870 870 or user group name
871 871 """
872 872 repo = self._get_repo(repo)
873 873 group_name = self._get_user_group(group_name)
874 874
875 875 obj = self.sa.query(UserGroupRepoToPerm) \
876 876 .filter(UserGroupRepoToPerm.repository == repo) \
877 877 .filter(UserGroupRepoToPerm.users_group == group_name) \
878 878 .scalar()
879 879 if obj:
880 880 self.sa.delete(obj)
881 881 log.debug('Revoked perm to %s on %s', repo, group_name)
882 882 action_logger_generic(
883 883 'revoked permission from usergroup: {} on repo: {}'.format(
884 884 group_name, repo), namespace='security.repo')
885 885
886 886 def delete_stats(self, repo_name):
887 887 """
888 888 removes stats for given repo
889 889
890 890 :param repo_name:
891 891 """
892 892 repo = self._get_repo(repo_name)
893 893 try:
894 894 obj = self.sa.query(Statistics) \
895 895 .filter(Statistics.repository == repo).scalar()
896 896 if obj:
897 897 self.sa.delete(obj)
898 898 except Exception:
899 899 log.error(traceback.format_exc())
900 900 raise
901 901
902 902 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
903 903 field_type='str', field_desc=''):
904 904
905 905 repo = self._get_repo(repo_name)
906 906
907 907 new_field = RepositoryField()
908 908 new_field.repository = repo
909 909 new_field.field_key = field_key
910 910 new_field.field_type = field_type # python type
911 911 new_field.field_value = field_value
912 912 new_field.field_desc = field_desc
913 913 new_field.field_label = field_label
914 914 self.sa.add(new_field)
915 915 return new_field
916 916
917 917 def delete_repo_field(self, repo_name, field_key):
918 918 repo = self._get_repo(repo_name)
919 919 field = RepositoryField.get_by_key_name(field_key, repo)
920 920 if field:
921 921 self.sa.delete(field)
922 922
923 923 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
924 924 clone_uri=None, repo_store_location=None,
925 925 use_global_config=False, install_hooks=True):
926 926 """
927 927 makes repository on filesystem. It's group aware means it'll create
928 928 a repository within a group, and alter the paths accordingly of
929 929 group location
930 930
931 931 :param repo_name:
932 932 :param alias:
933 933 :param parent:
934 934 :param clone_uri:
935 935 :param repo_store_location:
936 936 """
937 937 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
938 938 from rhodecode.model.scm import ScmModel
939 939
940 940 if Repository.NAME_SEP in repo_name:
941 941 raise ValueError(
942 942 'repo_name must not contain groups got `%s`' % repo_name)
943 943
944 944 if isinstance(repo_group, RepoGroup):
945 945 new_parent_path = os.sep.join(repo_group.full_path_splitted)
946 946 else:
947 947 new_parent_path = repo_group or ''
948 948
949 949 if repo_store_location:
950 950 _paths = [repo_store_location]
951 951 else:
952 952 _paths = [self.repos_path, new_parent_path, repo_name]
953 953 # we need to make it str for mercurial
954 954 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
955 955
956 956 # check if this path is not a repository
957 957 if is_valid_repo(repo_path, self.repos_path):
958 958 raise Exception('This path %s is a valid repository' % repo_path)
959 959
960 960 # check if this path is a group
961 961 if is_valid_repo_group(repo_path, self.repos_path):
962 962 raise Exception('This path %s is a valid group' % repo_path)
963 963
964 964 log.info('creating repo %s in %s from url: `%s`',
965 965 repo_name, safe_unicode(repo_path),
966 966 obfuscate_url_pw(clone_uri))
967 967
968 968 backend = get_backend(repo_type)
969 969
970 970 config_repo = None if use_global_config else repo_name
971 971 if config_repo and new_parent_path:
972 972 config_repo = Repository.NAME_SEP.join(
973 973 (new_parent_path, config_repo))
974 974 config = make_db_config(clear_session=False, repo=config_repo)
975 975 config.set('extensions', 'largefiles', '')
976 976
977 977 # patch and reset hooks section of UI config to not run any
978 978 # hooks on creating remote repo
979 979 config.clear_section('hooks')
980 980
981 981 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
982 982 if repo_type == 'git':
983 983 repo = backend(
984 984 repo_path, config=config, create=True, src_url=clone_uri, bare=True,
985 985 with_wire={"cache": False})
986 986 else:
987 987 repo = backend(
988 988 repo_path, config=config, create=True, src_url=clone_uri,
989 989 with_wire={"cache": False})
990 990
991 991 if install_hooks:
992 992 repo.install_hooks()
993 993
994 994 log.debug('Created repo %s with %s backend',
995 995 safe_unicode(repo_name), safe_unicode(repo_type))
996 996 return repo
997 997
998 998 def _rename_filesystem_repo(self, old, new):
999 999 """
1000 1000 renames repository on filesystem
1001 1001
1002 1002 :param old: old name
1003 1003 :param new: new name
1004 1004 """
1005 1005 log.info('renaming repo from %s to %s', old, new)
1006 1006
1007 1007 old_path = os.path.join(self.repos_path, old)
1008 1008 new_path = os.path.join(self.repos_path, new)
1009 1009 if os.path.isdir(new_path):
1010 1010 raise Exception(
1011 1011 'Was trying to rename to already existing dir %s' % new_path
1012 1012 )
1013 1013 shutil.move(old_path, new_path)
1014 1014
1015 1015 def _delete_filesystem_repo(self, repo):
1016 1016 """
1017 1017 removes repo from filesystem, the removal is acctually made by
1018 1018 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
1019 1019 repository is no longer valid for rhodecode, can be undeleted later on
1020 1020 by reverting the renames on this repository
1021 1021
1022 1022 :param repo: repo object
1023 1023 """
1024 1024 rm_path = os.path.join(self.repos_path, repo.repo_name)
1025 1025 repo_group = repo.group
1026 1026 log.info("Removing repository %s", rm_path)
1027 1027 # disable hg/git internal that it doesn't get detected as repo
1028 1028 alias = repo.repo_type
1029 1029
1030 1030 config = make_db_config(clear_session=False)
1031 1031 config.set('extensions', 'largefiles', '')
1032 1032 bare = getattr(repo.scm_instance(config=config), 'bare', False)
1033 1033
1034 1034 # skip this for bare git repos
1035 1035 if not bare:
1036 1036 # disable VCS repo
1037 1037 vcs_path = os.path.join(rm_path, '.%s' % alias)
1038 1038 if os.path.exists(vcs_path):
1039 1039 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
1040 1040
1041 1041 _now = datetime.datetime.now()
1042 1042 _ms = str(_now.microsecond).rjust(6, '0')
1043 1043 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
1044 1044 repo.just_name)
1045 1045 if repo_group:
1046 1046 # if repository is in group, prefix the removal path with the group
1047 1047 args = repo_group.full_path_splitted + [_d]
1048 1048 _d = os.path.join(*args)
1049 1049
1050 1050 if os.path.isdir(rm_path):
1051 1051 shutil.move(rm_path, os.path.join(self.repos_path, _d))
1052 1052
1053 1053 # finally cleanup diff-cache if it exists
1054 1054 cached_diffs_dir = repo.cached_diffs_dir
1055 1055 if os.path.isdir(cached_diffs_dir):
1056 1056 shutil.rmtree(cached_diffs_dir)
1057 1057
1058 1058
1059 1059 class ReadmeFinder:
1060 1060 """
1061 1061 Utility which knows how to find a readme for a specific commit.
1062 1062
1063 1063 The main idea is that this is a configurable algorithm. When creating an
1064 1064 instance you can define parameters, currently only the `default_renderer`.
1065 1065 Based on this configuration the method :meth:`search` behaves slightly
1066 1066 different.
1067 1067 """
1068 1068
1069 1069 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
1070 1070 path_re = re.compile(r'^docs?', re.IGNORECASE)
1071 1071
1072 1072 default_priorities = {
1073 1073 None: 0,
1074 1074 '.text': 2,
1075 1075 '.txt': 3,
1076 1076 '.rst': 1,
1077 1077 '.rest': 2,
1078 1078 '.md': 1,
1079 1079 '.mkdn': 2,
1080 1080 '.mdown': 3,
1081 1081 '.markdown': 4,
1082 1082 }
1083 1083
1084 1084 path_priority = {
1085 1085 'doc': 0,
1086 1086 'docs': 1,
1087 1087 }
1088 1088
1089 1089 FALLBACK_PRIORITY = 99
1090 1090
1091 1091 RENDERER_TO_EXTENSION = {
1092 1092 'rst': ['.rst', '.rest'],
1093 1093 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
1094 1094 }
1095 1095
1096 1096 def __init__(self, default_renderer=None):
1097 1097 self._default_renderer = default_renderer
1098 1098 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
1099 1099 default_renderer, [])
1100 1100
1101 1101 def search(self, commit, path=u'/'):
1102 1102 """
1103 1103 Find a readme in the given `commit`.
1104 1104 """
1105 1105 nodes = commit.get_nodes(path)
1106 1106 matches = self._match_readmes(nodes)
1107 1107 matches = self._sort_according_to_priority(matches)
1108 1108 if matches:
1109 1109 return matches[0].node
1110 1110
1111 1111 paths = self._match_paths(nodes)
1112 1112 paths = self._sort_paths_according_to_priority(paths)
1113 1113 for path in paths:
1114 1114 match = self.search(commit, path=path)
1115 1115 if match:
1116 1116 return match
1117 1117
1118 1118 return None
1119 1119
1120 1120 def _match_readmes(self, nodes):
1121 1121 for node in nodes:
1122 1122 if not node.is_file():
1123 1123 continue
1124 1124 path = node.path.rsplit('/', 1)[-1]
1125 1125 match = self.readme_re.match(path)
1126 1126 if match:
1127 1127 extension = match.group(1)
1128 1128 yield ReadmeMatch(node, match, self._priority(extension))
1129 1129
1130 1130 def _match_paths(self, nodes):
1131 1131 for node in nodes:
1132 1132 if not node.is_dir():
1133 1133 continue
1134 1134 match = self.path_re.match(node.path)
1135 1135 if match:
1136 1136 yield node.path
1137 1137
1138 1138 def _priority(self, extension):
1139 1139 renderer_priority = (
1140 1140 0 if extension in self._renderer_extensions else 1)
1141 1141 extension_priority = self.default_priorities.get(
1142 1142 extension, self.FALLBACK_PRIORITY)
1143 1143 return (renderer_priority, extension_priority)
1144 1144
1145 1145 def _sort_according_to_priority(self, matches):
1146 1146
1147 1147 def priority_and_path(match):
1148 1148 return (match.priority, match.path)
1149 1149
1150 1150 return sorted(matches, key=priority_and_path)
1151 1151
1152 1152 def _sort_paths_according_to_priority(self, paths):
1153 1153
1154 1154 def priority_and_path(path):
1155 1155 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1156 1156
1157 1157 return sorted(paths, key=priority_and_path)
1158 1158
1159 1159
1160 1160 class ReadmeMatch:
1161 1161
1162 1162 def __init__(self, node, match, priority):
1163 1163 self.node = node
1164 1164 self._match = match
1165 1165 self.priority = priority
1166 1166
1167 1167 @property
1168 1168 def path(self):
1169 1169 return self.node.path
1170 1170
1171 1171 def __repr__(self):
1172 1172 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,884 +1,884 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 repo group model for RhodeCode
24 24 """
25 25
26 26 import os
27 27 import datetime
28 28 import itertools
29 29 import logging
30 30 import shutil
31 31 import time
32 32 import traceback
33 33 import string
34 34
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from rhodecode import events
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import (_hash_key, func, or_, in_filter_generator,
40 40 Session, RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
41 41 UserGroup, Repository)
42 42 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
43 43 from rhodecode.lib.caching_query import FromCache
44 44 from rhodecode.lib.utils2 import action_logger_generic
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class RepoGroupModel(BaseModel):
50 50
51 51 cls = RepoGroup
52 52 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
53 53 PERSONAL_GROUP_PATTERN = '${username}' # default
54 54
55 55 def _get_user_group(self, users_group):
56 56 return self._get_instance(UserGroup, users_group,
57 57 callback=UserGroup.get_by_group_name)
58 58
59 59 def _get_repo_group(self, repo_group):
60 60 return self._get_instance(RepoGroup, repo_group,
61 61 callback=RepoGroup.get_by_group_name)
62 62
63 63 @LazyProperty
64 64 def repos_path(self):
65 65 """
66 66 Gets the repositories root path from database
67 67 """
68 68
69 69 settings_model = VcsSettingsModel(sa=self.sa)
70 70 return settings_model.get_repos_location()
71 71
72 72 def get_by_group_name(self, repo_group_name, cache=None):
73 73 repo = self.sa.query(RepoGroup) \
74 74 .filter(RepoGroup.group_name == repo_group_name)
75 75
76 76 if cache:
77 77 name_key = _hash_key(repo_group_name)
78 78 repo = repo.options(
79 79 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
80 80 return repo.scalar()
81 81
82 82 def get_default_create_personal_repo_group(self):
83 83 value = SettingsModel().get_setting_by_name(
84 84 'create_personal_repo_group')
85 85 return value.app_settings_value if value else None or False
86 86
87 87 def get_personal_group_name_pattern(self):
88 88 value = SettingsModel().get_setting_by_name(
89 89 'personal_repo_group_pattern')
90 90 val = value.app_settings_value if value else None
91 91 group_template = val or self.PERSONAL_GROUP_PATTERN
92 92
93 93 group_template = group_template.lstrip('/')
94 94 return group_template
95 95
96 96 def get_personal_group_name(self, user):
97 97 template = self.get_personal_group_name_pattern()
98 98 return string.Template(template).safe_substitute(
99 99 username=user.username,
100 100 user_id=user.user_id,
101 101 first_name=user.first_name,
102 102 last_name=user.last_name,
103 103 )
104 104
105 105 def create_personal_repo_group(self, user, commit_early=True):
106 106 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
107 107 personal_repo_group_name = self.get_personal_group_name(user)
108 108
109 109 # create a new one
110 110 RepoGroupModel().create(
111 111 group_name=personal_repo_group_name,
112 112 group_description=desc,
113 113 owner=user.username,
114 114 personal=True,
115 115 commit_early=commit_early)
116 116
117 117 def _create_default_perms(self, new_group):
118 118 # create default permission
119 119 default_perm = 'group.read'
120 120 def_user = User.get_default_user()
121 121 for p in def_user.user_perms:
122 122 if p.permission.permission_name.startswith('group.'):
123 123 default_perm = p.permission.permission_name
124 124 break
125 125
126 126 repo_group_to_perm = UserRepoGroupToPerm()
127 127 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
128 128
129 129 repo_group_to_perm.group = new_group
130 130 repo_group_to_perm.user_id = def_user.user_id
131 131 return repo_group_to_perm
132 132
133 133 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
134 134 get_object=False):
135 135 """
136 136 Get's the group name and a parent group name from given group name.
137 137 If repo_in_path is set to truth, we asume the full path also includes
138 138 repo name, in such case we clean the last element.
139 139
140 140 :param group_name_full:
141 141 """
142 142 split_paths = 1
143 143 if repo_in_path:
144 144 split_paths = 2
145 145 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
146 146
147 147 if repo_in_path and len(_parts) > 1:
148 148 # such case last element is the repo_name
149 149 _parts.pop(-1)
150 150 group_name_cleaned = _parts[-1] # just the group name
151 151 parent_repo_group_name = None
152 152
153 153 if len(_parts) > 1:
154 154 parent_repo_group_name = _parts[0]
155 155
156 156 parent_group = None
157 157 if parent_repo_group_name:
158 158 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
159 159
160 160 if get_object:
161 161 return group_name_cleaned, parent_repo_group_name, parent_group
162 162
163 163 return group_name_cleaned, parent_repo_group_name
164 164
165 165 def check_exist_filesystem(self, group_name, exc_on_failure=True):
166 166 create_path = os.path.join(self.repos_path, group_name)
167 167 log.debug('creating new group in %s', create_path)
168 168
169 169 if os.path.isdir(create_path):
170 170 if exc_on_failure:
171 171 abs_create_path = os.path.abspath(create_path)
172 172 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
173 173 return False
174 174 return True
175 175
176 176 def _create_group(self, group_name):
177 177 """
178 178 makes repository group on filesystem
179 179
180 180 :param repo_name:
181 181 :param parent_id:
182 182 """
183 183
184 184 self.check_exist_filesystem(group_name)
185 185 create_path = os.path.join(self.repos_path, group_name)
186 186 log.debug('creating new group in %s', create_path)
187 187 os.makedirs(create_path, mode=0o755)
188 188 log.debug('created group in %s', create_path)
189 189
190 190 def _rename_group(self, old, new):
191 191 """
192 192 Renames a group on filesystem
193 193
194 194 :param group_name:
195 195 """
196 196
197 197 if old == new:
198 198 log.debug('skipping group rename')
199 199 return
200 200
201 201 log.debug('renaming repository group from %s to %s', old, new)
202 202
203 203 old_path = os.path.join(self.repos_path, old)
204 204 new_path = os.path.join(self.repos_path, new)
205 205
206 206 log.debug('renaming repos paths from %s to %s', old_path, new_path)
207 207
208 208 if os.path.isdir(new_path):
209 209 raise Exception('Was trying to rename to already '
210 210 'existing dir %s' % new_path)
211 211 shutil.move(old_path, new_path)
212 212
213 213 def _delete_filesystem_group(self, group, force_delete=False):
214 214 """
215 215 Deletes a group from a filesystem
216 216
217 217 :param group: instance of group from database
218 218 :param force_delete: use shutil rmtree to remove all objects
219 219 """
220 220 paths = group.full_path.split(RepoGroup.url_sep())
221 221 paths = os.sep.join(paths)
222 222
223 223 rm_path = os.path.join(self.repos_path, paths)
224 224 log.info("Removing group %s", rm_path)
225 225 # delete only if that path really exists
226 226 if os.path.isdir(rm_path):
227 227 if force_delete:
228 228 shutil.rmtree(rm_path)
229 229 else:
230 230 # archive that group`
231 231 _now = datetime.datetime.now()
232 232 _ms = str(_now.microsecond).rjust(6, '0')
233 233 _d = 'rm__%s_GROUP_%s' % (
234 234 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
235 235 shutil.move(rm_path, os.path.join(self.repos_path, _d))
236 236
237 237 def create(self, group_name, group_description, owner, just_db=False,
238 238 copy_permissions=False, personal=None, commit_early=True):
239 239
240 240 (group_name_cleaned,
241 241 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
242 242
243 243 parent_group = None
244 244 if parent_group_name:
245 245 parent_group = self._get_repo_group(parent_group_name)
246 246 if not parent_group:
247 247 # we tried to create a nested group, but the parent is not
248 248 # existing
249 249 raise ValueError(
250 250 'Parent group `%s` given in `%s` group name '
251 251 'is not yet existing.' % (parent_group_name, group_name))
252 252
253 253 # because we are doing a cleanup, we need to check if such directory
254 254 # already exists. If we don't do that we can accidentally delete
255 255 # existing directory via cleanup that can cause data issues, since
256 256 # delete does a folder rename to special syntax later cleanup
257 257 # functions can delete this
258 258 cleanup_group = self.check_exist_filesystem(group_name,
259 259 exc_on_failure=False)
260 260 user = self._get_user(owner)
261 261 if not user:
262 262 raise ValueError('Owner %s not found as rhodecode user', owner)
263 263
264 264 try:
265 265 new_repo_group = RepoGroup()
266 266 new_repo_group.user = user
267 267 new_repo_group.group_description = group_description or group_name
268 268 new_repo_group.parent_group = parent_group
269 269 new_repo_group.group_name = group_name
270 270 new_repo_group.personal = personal
271 271
272 272 self.sa.add(new_repo_group)
273 273
274 274 # create an ADMIN permission for owner except if we're super admin,
275 275 # later owner should go into the owner field of groups
276 276 if not user.is_admin:
277 277 self.grant_user_permission(repo_group=new_repo_group,
278 278 user=owner, perm='group.admin')
279 279
280 280 if parent_group and copy_permissions:
281 281 # copy permissions from parent
282 282 user_perms = UserRepoGroupToPerm.query() \
283 283 .filter(UserRepoGroupToPerm.group == parent_group).all()
284 284
285 285 group_perms = UserGroupRepoGroupToPerm.query() \
286 286 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
287 287
288 288 for perm in user_perms:
289 289 # don't copy over the permission for user who is creating
290 290 # this group, if he is not super admin he get's admin
291 291 # permission set above
292 292 if perm.user != user or user.is_admin:
293 293 UserRepoGroupToPerm.create(
294 294 perm.user, new_repo_group, perm.permission)
295 295
296 296 for perm in group_perms:
297 297 UserGroupRepoGroupToPerm.create(
298 298 perm.users_group, new_repo_group, perm.permission)
299 299 else:
300 300 perm_obj = self._create_default_perms(new_repo_group)
301 301 self.sa.add(perm_obj)
302 302
303 303 # now commit the changes, earlier so we are sure everything is in
304 304 # the database.
305 305 if commit_early:
306 306 self.sa.commit()
307 307 if not just_db:
308 308 self._create_group(new_repo_group.group_name)
309 309
310 310 # trigger the post hook
311 from rhodecode.lib.hooks_base import log_create_repository_group
311 from rhodecode.lib import hooks_base
312 312 repo_group = RepoGroup.get_by_group_name(group_name)
313 313
314 314 # update repo group commit caches initially
315 315 repo_group.update_commit_cache()
316 316
317 log_create_repository_group(
317 hooks_base.create_repository_group(
318 318 created_by=user.username, **repo_group.get_dict())
319 319
320 320 # Trigger create event.
321 321 events.trigger(events.RepoGroupCreateEvent(repo_group))
322 322
323 323 return new_repo_group
324 324 except Exception:
325 325 self.sa.rollback()
326 326 log.exception('Exception occurred when creating repository group, '
327 327 'doing cleanup...')
328 328 # rollback things manually !
329 329 repo_group = RepoGroup.get_by_group_name(group_name)
330 330 if repo_group:
331 331 RepoGroup.delete(repo_group.group_id)
332 332 self.sa.commit()
333 333 if cleanup_group:
334 334 RepoGroupModel()._delete_filesystem_group(repo_group)
335 335 raise
336 336
337 337 def update_permissions(
338 338 self, repo_group, perm_additions=None, perm_updates=None,
339 339 perm_deletions=None, recursive=None, check_perms=True,
340 340 cur_user=None):
341 341 from rhodecode.model.repo import RepoModel
342 342 from rhodecode.lib.auth import HasUserGroupPermissionAny
343 343
344 344 if not perm_additions:
345 345 perm_additions = []
346 346 if not perm_updates:
347 347 perm_updates = []
348 348 if not perm_deletions:
349 349 perm_deletions = []
350 350
351 351 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
352 352
353 353 changes = {
354 354 'added': [],
355 355 'updated': [],
356 356 'deleted': [],
357 357 'default_user_changed': None
358 358 }
359 359
360 360 def _set_perm_user(obj, user, perm):
361 361 if isinstance(obj, RepoGroup):
362 362 self.grant_user_permission(
363 363 repo_group=obj, user=user, perm=perm)
364 364 elif isinstance(obj, Repository):
365 365 # private repos will not allow to change the default
366 366 # permissions using recursive mode
367 367 if obj.private and user == User.DEFAULT_USER:
368 368 return
369 369
370 370 # we set group permission but we have to switch to repo
371 371 # permission
372 372 perm = perm.replace('group.', 'repository.')
373 373 RepoModel().grant_user_permission(
374 374 repo=obj, user=user, perm=perm)
375 375
376 376 def _set_perm_group(obj, users_group, perm):
377 377 if isinstance(obj, RepoGroup):
378 378 self.grant_user_group_permission(
379 379 repo_group=obj, group_name=users_group, perm=perm)
380 380 elif isinstance(obj, Repository):
381 381 # we set group permission but we have to switch to repo
382 382 # permission
383 383 perm = perm.replace('group.', 'repository.')
384 384 RepoModel().grant_user_group_permission(
385 385 repo=obj, group_name=users_group, perm=perm)
386 386
387 387 def _revoke_perm_user(obj, user):
388 388 if isinstance(obj, RepoGroup):
389 389 self.revoke_user_permission(repo_group=obj, user=user)
390 390 elif isinstance(obj, Repository):
391 391 RepoModel().revoke_user_permission(repo=obj, user=user)
392 392
393 393 def _revoke_perm_group(obj, user_group):
394 394 if isinstance(obj, RepoGroup):
395 395 self.revoke_user_group_permission(
396 396 repo_group=obj, group_name=user_group)
397 397 elif isinstance(obj, Repository):
398 398 RepoModel().revoke_user_group_permission(
399 399 repo=obj, group_name=user_group)
400 400
401 401 # start updates
402 402 log.debug('Now updating permissions for %s in recursive mode:%s',
403 403 repo_group, recursive)
404 404
405 405 # initialize check function, we'll call that multiple times
406 406 has_group_perm = HasUserGroupPermissionAny(*req_perms)
407 407
408 408 for obj in repo_group.recursive_groups_and_repos():
409 409 # iterated obj is an instance of a repos group or repository in
410 410 # that group, recursive option can be: none, repos, groups, all
411 411 if recursive == 'all':
412 412 obj = obj
413 413 elif recursive == 'repos':
414 414 # skip groups, other than this one
415 415 if isinstance(obj, RepoGroup) and not obj == repo_group:
416 416 continue
417 417 elif recursive == 'groups':
418 418 # skip repos
419 419 if isinstance(obj, Repository):
420 420 continue
421 421 else: # recursive == 'none':
422 422 # DEFAULT option - don't apply to iterated objects
423 423 # also we do a break at the end of this loop. if we are not
424 424 # in recursive mode
425 425 obj = repo_group
426 426
427 427 change_obj = obj.get_api_data()
428 428
429 429 # update permissions
430 430 for member_id, perm, member_type in perm_updates:
431 431 member_id = int(member_id)
432 432 if member_type == 'user':
433 433 member_name = User.get(member_id).username
434 434 if isinstance(obj, RepoGroup) and obj == repo_group and member_name == User.DEFAULT_USER:
435 435 # NOTE(dan): detect if we changed permissions for default user
436 436 perm_obj = self.sa.query(UserRepoGroupToPerm) \
437 437 .filter(UserRepoGroupToPerm.user_id == member_id) \
438 438 .filter(UserRepoGroupToPerm.group == repo_group) \
439 439 .scalar()
440 440 if perm_obj and perm_obj.permission.permission_name != perm:
441 441 changes['default_user_changed'] = True
442 442
443 443 # this updates also current one if found
444 444 _set_perm_user(obj, user=member_id, perm=perm)
445 445 elif member_type == 'user_group':
446 446 member_name = UserGroup.get(member_id).users_group_name
447 447 if not check_perms or has_group_perm(member_name,
448 448 user=cur_user):
449 449 _set_perm_group(obj, users_group=member_id, perm=perm)
450 450 else:
451 451 raise ValueError("member_type must be 'user' or 'user_group' "
452 452 "got {} instead".format(member_type))
453 453
454 454 changes['updated'].append(
455 455 {'change_obj': change_obj, 'type': member_type,
456 456 'id': member_id, 'name': member_name, 'new_perm': perm})
457 457
458 458 # set new permissions
459 459 for member_id, perm, member_type in perm_additions:
460 460 member_id = int(member_id)
461 461 if member_type == 'user':
462 462 member_name = User.get(member_id).username
463 463 _set_perm_user(obj, user=member_id, perm=perm)
464 464 elif member_type == 'user_group':
465 465 # check if we have permissions to alter this usergroup
466 466 member_name = UserGroup.get(member_id).users_group_name
467 467 if not check_perms or has_group_perm(member_name,
468 468 user=cur_user):
469 469 _set_perm_group(obj, users_group=member_id, perm=perm)
470 470 else:
471 471 raise ValueError("member_type must be 'user' or 'user_group' "
472 472 "got {} instead".format(member_type))
473 473
474 474 changes['added'].append(
475 475 {'change_obj': change_obj, 'type': member_type,
476 476 'id': member_id, 'name': member_name, 'new_perm': perm})
477 477
478 478 # delete permissions
479 479 for member_id, perm, member_type in perm_deletions:
480 480 member_id = int(member_id)
481 481 if member_type == 'user':
482 482 member_name = User.get(member_id).username
483 483 _revoke_perm_user(obj, user=member_id)
484 484 elif member_type == 'user_group':
485 485 # check if we have permissions to alter this usergroup
486 486 member_name = UserGroup.get(member_id).users_group_name
487 487 if not check_perms or has_group_perm(member_name,
488 488 user=cur_user):
489 489 _revoke_perm_group(obj, user_group=member_id)
490 490 else:
491 491 raise ValueError("member_type must be 'user' or 'user_group' "
492 492 "got {} instead".format(member_type))
493 493
494 494 changes['deleted'].append(
495 495 {'change_obj': change_obj, 'type': member_type,
496 496 'id': member_id, 'name': member_name, 'new_perm': perm})
497 497
498 498 # if it's not recursive call for all,repos,groups
499 499 # break the loop and don't proceed with other changes
500 500 if recursive not in ['all', 'repos', 'groups']:
501 501 break
502 502
503 503 return changes
504 504
505 505 def update(self, repo_group, form_data):
506 506 try:
507 507 repo_group = self._get_repo_group(repo_group)
508 508 old_path = repo_group.full_path
509 509
510 510 # change properties
511 511 if 'group_description' in form_data:
512 512 repo_group.group_description = form_data['group_description']
513 513
514 514 if 'enable_locking' in form_data:
515 515 repo_group.enable_locking = form_data['enable_locking']
516 516
517 517 if 'group_parent_id' in form_data:
518 518 parent_group = (
519 519 self._get_repo_group(form_data['group_parent_id']))
520 520 repo_group.group_parent_id = (
521 521 parent_group.group_id if parent_group else None)
522 522 repo_group.parent_group = parent_group
523 523
524 524 # mikhail: to update the full_path, we have to explicitly
525 525 # update group_name
526 526 group_name = form_data.get('group_name', repo_group.name)
527 527 repo_group.group_name = repo_group.get_new_name(group_name)
528 528
529 529 new_path = repo_group.full_path
530 530
531 531 if 'user' in form_data:
532 532 repo_group.user = User.get_by_username(form_data['user'])
533 533
534 534 self.sa.add(repo_group)
535 535
536 536 # iterate over all members of this groups and do fixes
537 537 # set locking if given
538 538 # if obj is a repoGroup also fix the name of the group according
539 539 # to the parent
540 540 # if obj is a Repo fix it's name
541 541 # this can be potentially heavy operation
542 542 for obj in repo_group.recursive_groups_and_repos():
543 543 # set the value from it's parent
544 544 obj.enable_locking = repo_group.enable_locking
545 545 if isinstance(obj, RepoGroup):
546 546 new_name = obj.get_new_name(obj.name)
547 547 log.debug('Fixing group %s to new name %s',
548 548 obj.group_name, new_name)
549 549 obj.group_name = new_name
550 550
551 551 elif isinstance(obj, Repository):
552 552 # we need to get all repositories from this new group and
553 553 # rename them accordingly to new group path
554 554 new_name = obj.get_new_name(obj.just_name)
555 555 log.debug('Fixing repo %s to new name %s',
556 556 obj.repo_name, new_name)
557 557 obj.repo_name = new_name
558 558
559 559 self.sa.add(obj)
560 560
561 561 self._rename_group(old_path, new_path)
562 562
563 563 # Trigger update event.
564 564 events.trigger(events.RepoGroupUpdateEvent(repo_group))
565 565
566 566 return repo_group
567 567 except Exception:
568 568 log.error(traceback.format_exc())
569 569 raise
570 570
571 571 def delete(self, repo_group, force_delete=False, fs_remove=True):
572 572 repo_group = self._get_repo_group(repo_group)
573 573 if not repo_group:
574 574 return False
575 575 try:
576 576 self.sa.delete(repo_group)
577 577 if fs_remove:
578 578 self._delete_filesystem_group(repo_group, force_delete)
579 579 else:
580 580 log.debug('skipping removal from filesystem')
581 581
582 582 # Trigger delete event.
583 583 events.trigger(events.RepoGroupDeleteEvent(repo_group))
584 584 return True
585 585
586 586 except Exception:
587 587 log.error('Error removing repo_group %s', repo_group)
588 588 raise
589 589
590 590 def grant_user_permission(self, repo_group, user, perm):
591 591 """
592 592 Grant permission for user on given repository group, or update
593 593 existing one if found
594 594
595 595 :param repo_group: Instance of RepoGroup, repositories_group_id,
596 596 or repositories_group name
597 597 :param user: Instance of User, user_id or username
598 598 :param perm: Instance of Permission, or permission_name
599 599 """
600 600
601 601 repo_group = self._get_repo_group(repo_group)
602 602 user = self._get_user(user)
603 603 permission = self._get_perm(perm)
604 604
605 605 # check if we have that permission already
606 606 obj = self.sa.query(UserRepoGroupToPerm)\
607 607 .filter(UserRepoGroupToPerm.user == user)\
608 608 .filter(UserRepoGroupToPerm.group == repo_group)\
609 609 .scalar()
610 610 if obj is None:
611 611 # create new !
612 612 obj = UserRepoGroupToPerm()
613 613 obj.group = repo_group
614 614 obj.user = user
615 615 obj.permission = permission
616 616 self.sa.add(obj)
617 617 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
618 618 action_logger_generic(
619 619 'granted permission: {} to user: {} on repogroup: {}'.format(
620 620 perm, user, repo_group), namespace='security.repogroup')
621 621 return obj
622 622
623 623 def revoke_user_permission(self, repo_group, user):
624 624 """
625 625 Revoke permission for user on given repository group
626 626
627 627 :param repo_group: Instance of RepoGroup, repositories_group_id,
628 628 or repositories_group name
629 629 :param user: Instance of User, user_id or username
630 630 """
631 631
632 632 repo_group = self._get_repo_group(repo_group)
633 633 user = self._get_user(user)
634 634
635 635 obj = self.sa.query(UserRepoGroupToPerm)\
636 636 .filter(UserRepoGroupToPerm.user == user)\
637 637 .filter(UserRepoGroupToPerm.group == repo_group)\
638 638 .scalar()
639 639 if obj:
640 640 self.sa.delete(obj)
641 641 log.debug('Revoked perm on %s on %s', repo_group, user)
642 642 action_logger_generic(
643 643 'revoked permission from user: {} on repogroup: {}'.format(
644 644 user, repo_group), namespace='security.repogroup')
645 645
646 646 def grant_user_group_permission(self, repo_group, group_name, perm):
647 647 """
648 648 Grant permission for user group on given repository group, or update
649 649 existing one if found
650 650
651 651 :param repo_group: Instance of RepoGroup, repositories_group_id,
652 652 or repositories_group name
653 653 :param group_name: Instance of UserGroup, users_group_id,
654 654 or user group name
655 655 :param perm: Instance of Permission, or permission_name
656 656 """
657 657 repo_group = self._get_repo_group(repo_group)
658 658 group_name = self._get_user_group(group_name)
659 659 permission = self._get_perm(perm)
660 660
661 661 # check if we have that permission already
662 662 obj = self.sa.query(UserGroupRepoGroupToPerm)\
663 663 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
664 664 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
665 665 .scalar()
666 666
667 667 if obj is None:
668 668 # create new
669 669 obj = UserGroupRepoGroupToPerm()
670 670
671 671 obj.group = repo_group
672 672 obj.users_group = group_name
673 673 obj.permission = permission
674 674 self.sa.add(obj)
675 675 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
676 676 action_logger_generic(
677 677 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
678 678 perm, group_name, repo_group), namespace='security.repogroup')
679 679 return obj
680 680
681 681 def revoke_user_group_permission(self, repo_group, group_name):
682 682 """
683 683 Revoke permission for user group on given repository group
684 684
685 685 :param repo_group: Instance of RepoGroup, repositories_group_id,
686 686 or repositories_group name
687 687 :param group_name: Instance of UserGroup, users_group_id,
688 688 or user group name
689 689 """
690 690 repo_group = self._get_repo_group(repo_group)
691 691 group_name = self._get_user_group(group_name)
692 692
693 693 obj = self.sa.query(UserGroupRepoGroupToPerm)\
694 694 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
695 695 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
696 696 .scalar()
697 697 if obj:
698 698 self.sa.delete(obj)
699 699 log.debug('Revoked perm to %s on %s', repo_group, group_name)
700 700 action_logger_generic(
701 701 'revoked permission from usergroup: {} on repogroup: {}'.format(
702 702 group_name, repo_group), namespace='security.repogroup')
703 703
704 704 @classmethod
705 705 def update_commit_cache(cls, repo_groups=None):
706 706 if not repo_groups:
707 707 repo_groups = RepoGroup.getAll()
708 708 for repo_group in repo_groups:
709 709 repo_group.update_commit_cache()
710 710
711 711 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
712 712 super_user_actions=False):
713 713
714 714 from pyramid.threadlocal import get_current_request
715 715 _render = get_current_request().get_partial_renderer(
716 716 'rhodecode:templates/data_table/_dt_elements.mako')
717 717 c = _render.get_call_context()
718 718 h = _render.get_helpers()
719 719
720 720 def quick_menu(repo_group_name):
721 721 return _render('quick_repo_group_menu', repo_group_name)
722 722
723 723 def repo_group_lnk(repo_group_name):
724 724 return _render('repo_group_name', repo_group_name)
725 725
726 726 def last_change(last_change):
727 727 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
728 728 ts = time.time()
729 729 utc_offset = (datetime.datetime.fromtimestamp(ts)
730 730 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
731 731 last_change = last_change + datetime.timedelta(seconds=utc_offset)
732 732 return _render("last_change", last_change)
733 733
734 734 def desc(desc, personal):
735 735 return _render(
736 736 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
737 737
738 738 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
739 739 return _render(
740 740 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
741 741
742 742 def repo_group_name(repo_group_name, children_groups):
743 743 return _render("repo_group_name", repo_group_name, children_groups)
744 744
745 745 def user_profile(username):
746 746 return _render('user_profile', username)
747 747
748 748 repo_group_data = []
749 749 for group in repo_group_list:
750 750 # NOTE(marcink): because we use only raw column we need to load it like that
751 751 changeset_cache = RepoGroup._load_changeset_cache(
752 752 '', group._changeset_cache)
753 753 last_commit_change = RepoGroup._load_commit_change(changeset_cache)
754 754 row = {
755 755 "menu": quick_menu(group.group_name),
756 756 "name": repo_group_lnk(group.group_name),
757 757 "name_raw": group.group_name,
758 758
759 759 "last_change": last_change(last_commit_change),
760 760
761 761 "last_changeset": "",
762 762 "last_changeset_raw": "",
763 763
764 764 "desc": desc(h.escape(group.group_description), group.personal),
765 765 "top_level_repos": 0,
766 766 "owner": user_profile(group.User.username)
767 767 }
768 768 if admin:
769 769 repo_count = group.repositories.count()
770 770 children_groups = map(
771 771 h.safe_unicode,
772 772 itertools.chain((g.name for g in group.parents),
773 773 (x.name for x in [group])))
774 774 row.update({
775 775 "action": repo_group_actions(
776 776 group.group_id, group.group_name, repo_count),
777 777 "top_level_repos": repo_count,
778 778 "name": repo_group_name(group.group_name, children_groups),
779 779
780 780 })
781 781 repo_group_data.append(row)
782 782
783 783 return repo_group_data
784 784
785 785 def get_repo_groups_data_table(
786 786 self, draw, start, limit,
787 787 search_q, order_by, order_dir,
788 788 auth_user, repo_group_id):
789 789 from rhodecode.model.scm import RepoGroupList
790 790
791 791 _perms = ['group.read', 'group.write', 'group.admin']
792 792 repo_groups = RepoGroup.query() \
793 793 .filter(RepoGroup.group_parent_id == repo_group_id) \
794 794 .all()
795 795 auth_repo_group_list = RepoGroupList(
796 796 repo_groups, perm_set=_perms,
797 797 extra_kwargs=dict(user=auth_user))
798 798
799 799 allowed_ids = [-1]
800 800 for repo_group in auth_repo_group_list:
801 801 allowed_ids.append(repo_group.group_id)
802 802
803 803 repo_groups_data_total_count = RepoGroup.query() \
804 804 .filter(RepoGroup.group_parent_id == repo_group_id) \
805 805 .filter(or_(
806 806 # generate multiple IN to fix limitation problems
807 807 *in_filter_generator(RepoGroup.group_id, allowed_ids))
808 808 ) \
809 809 .count()
810 810
811 811 base_q = Session.query(
812 812 RepoGroup.group_name,
813 813 RepoGroup.group_name_hash,
814 814 RepoGroup.group_description,
815 815 RepoGroup.group_id,
816 816 RepoGroup.personal,
817 817 RepoGroup.updated_on,
818 818 RepoGroup._changeset_cache,
819 819 User,
820 820 ) \
821 821 .filter(RepoGroup.group_parent_id == repo_group_id) \
822 822 .filter(or_(
823 823 # generate multiple IN to fix limitation problems
824 824 *in_filter_generator(RepoGroup.group_id, allowed_ids))
825 825 ) \
826 826 .join(User, User.user_id == RepoGroup.user_id) \
827 827 .group_by(RepoGroup, User)
828 828
829 829 repo_groups_data_total_filtered_count = base_q.count()
830 830
831 831 sort_defined = False
832 832
833 833 if order_by == 'group_name':
834 834 sort_col = func.lower(RepoGroup.group_name)
835 835 sort_defined = True
836 836 elif order_by == 'user_username':
837 837 sort_col = User.username
838 838 else:
839 839 sort_col = getattr(RepoGroup, order_by, None)
840 840
841 841 if sort_defined or sort_col:
842 842 if order_dir == 'asc':
843 843 sort_col = sort_col.asc()
844 844 else:
845 845 sort_col = sort_col.desc()
846 846
847 847 base_q = base_q.order_by(sort_col)
848 848 base_q = base_q.offset(start).limit(limit)
849 849
850 850 repo_group_list = base_q.all()
851 851
852 852 repo_groups_data = RepoGroupModel().get_repo_groups_as_dict(
853 853 repo_group_list=repo_group_list, admin=False)
854 854
855 855 data = ({
856 856 'draw': draw,
857 857 'data': repo_groups_data,
858 858 'recordsTotal': repo_groups_data_total_count,
859 859 'recordsFiltered': repo_groups_data_total_filtered_count,
860 860 })
861 861 return data
862 862
863 863 def _get_defaults(self, repo_group_name):
864 864 repo_group = RepoGroup.get_by_group_name(repo_group_name)
865 865
866 866 if repo_group is None:
867 867 return None
868 868
869 869 defaults = repo_group.get_dict()
870 870 defaults['repo_group_name'] = repo_group.name
871 871 defaults['repo_group_description'] = repo_group.group_description
872 872 defaults['repo_group_enable_locking'] = repo_group.enable_locking
873 873
874 874 # we use -1 as this is how in HTML, we mark an empty group
875 875 defaults['repo_group'] = defaults['group_parent_id'] or -1
876 876
877 877 # fill owner
878 878 if repo_group.user:
879 879 defaults.update({'user': repo_group.user.username})
880 880 else:
881 881 replacement_user = User.get_first_super_admin().username
882 882 defaults.update({'user': replacement_user})
883 883
884 884 return defaults
@@ -1,1051 +1,1050 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
41 41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.auth_token import AuthTokenModel
49 49 from rhodecode.model.repo_group import RepoGroupModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'profile_link': h.link_to_user(user),
78 78 'value_display': h.escape(h.person(user)),
79 79 'value': user.username,
80 80 'value_type': 'user',
81 81 'active': user.active,
82 82 }
83 83
84 84 def get_users(self, name_contains=None, limit=20, only_active=True):
85 85
86 86 query = self.sa.query(User)
87 87 if only_active:
88 88 query = query.filter(User.active == true())
89 89
90 90 if name_contains:
91 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 92 query = query.filter(
93 93 or_(
94 94 User.name.ilike(ilike_expression),
95 95 User.lastname.ilike(ilike_expression),
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 99 query = query.limit(limit)
100 100 users = query.all()
101 101
102 102 _users = [
103 103 self._serialize_user(user) for user in users
104 104 ]
105 105 return _users
106 106
107 107 def get_by_username(self, username, cache=False, case_insensitive=False):
108 108
109 109 if case_insensitive:
110 110 user = self.sa.query(User).filter(User.username.ilike(username))
111 111 else:
112 112 user = self.sa.query(User)\
113 113 .filter(User.username == username)
114 114 if cache:
115 115 name_key = _hash_key(username)
116 116 user = user.options(
117 117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 118 return user.scalar()
119 119
120 120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 121 return User.get_by_email(email, case_insensitive, cache)
122 122
123 123 def get_by_auth_token(self, auth_token, cache=False):
124 124 return User.get_by_auth_token(auth_token, cache)
125 125
126 126 def get_active_user_count(self, cache=False):
127 127 qry = User.query().filter(
128 128 User.active == true()).filter(
129 129 User.username != User.DEFAULT_USER)
130 130 if cache:
131 131 qry = qry.options(
132 132 FromCache("sql_cache_short", "get_active_users"))
133 133 return qry.count()
134 134
135 135 def create(self, form_data, cur_user=None):
136 136 if not cur_user:
137 137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138 138
139 139 user_data = {
140 140 'username': form_data['username'],
141 141 'password': form_data['password'],
142 142 'email': form_data['email'],
143 143 'firstname': form_data['firstname'],
144 144 'lastname': form_data['lastname'],
145 145 'active': form_data['active'],
146 146 'extern_type': form_data['extern_type'],
147 147 'extern_name': form_data['extern_name'],
148 148 'admin': False,
149 149 'cur_user': cur_user
150 150 }
151 151
152 152 if 'create_repo_group' in form_data:
153 153 user_data['create_repo_group'] = str2bool(
154 154 form_data.get('create_repo_group'))
155 155
156 156 try:
157 157 if form_data.get('password_change'):
158 158 user_data['force_password_change'] = True
159 159 return UserModel().create_or_update(**user_data)
160 160 except Exception:
161 161 log.error(traceback.format_exc())
162 162 raise
163 163
164 164 def update_user(self, user, skip_attrs=None, **kwargs):
165 165 from rhodecode.lib.auth import get_crypt_password
166 166
167 167 user = self._get_user(user)
168 168 if user.username == User.DEFAULT_USER:
169 169 raise DefaultUserException(
170 170 "You can't edit this user (`%(username)s`) since it's "
171 171 "crucial for entire application" % {
172 172 'username': user.username})
173 173
174 174 # first store only defaults
175 175 user_attrs = {
176 176 'updating_user_id': user.user_id,
177 177 'username': user.username,
178 178 'password': user.password,
179 179 'email': user.email,
180 180 'firstname': user.name,
181 181 'lastname': user.lastname,
182 182 'description': user.description,
183 183 'active': user.active,
184 184 'admin': user.admin,
185 185 'extern_name': user.extern_name,
186 186 'extern_type': user.extern_type,
187 187 'language': user.user_data.get('language')
188 188 }
189 189
190 190 # in case there's new_password, that comes from form, use it to
191 191 # store password
192 192 if kwargs.get('new_password'):
193 193 kwargs['password'] = kwargs['new_password']
194 194
195 195 # cleanups, my_account password change form
196 196 kwargs.pop('current_password', None)
197 197 kwargs.pop('new_password', None)
198 198
199 199 # cleanups, user edit password change form
200 200 kwargs.pop('password_confirmation', None)
201 201 kwargs.pop('password_change', None)
202 202
203 203 # create repo group on user creation
204 204 kwargs.pop('create_repo_group', None)
205 205
206 206 # legacy forms send name, which is the firstname
207 207 firstname = kwargs.pop('name', None)
208 208 if firstname:
209 209 kwargs['firstname'] = firstname
210 210
211 211 for k, v in kwargs.items():
212 212 # skip if we don't want to update this
213 213 if skip_attrs and k in skip_attrs:
214 214 continue
215 215
216 216 user_attrs[k] = v
217 217
218 218 try:
219 219 return self.create_or_update(**user_attrs)
220 220 except Exception:
221 221 log.error(traceback.format_exc())
222 222 raise
223 223
224 224 def create_or_update(
225 225 self, username, password, email, firstname='', lastname='',
226 226 active=True, admin=False, extern_type=None, extern_name=None,
227 227 cur_user=None, plugin=None, force_password_change=False,
228 228 allow_to_create_user=True, create_repo_group=None,
229 229 updating_user_id=None, language=None, description='',
230 230 strict_creation_check=True):
231 231 """
232 232 Creates a new instance if not found, or updates current one
233 233
234 234 :param username:
235 235 :param password:
236 236 :param email:
237 237 :param firstname:
238 238 :param lastname:
239 239 :param active:
240 240 :param admin:
241 241 :param extern_type:
242 242 :param extern_name:
243 243 :param cur_user:
244 244 :param plugin: optional plugin this method was called from
245 245 :param force_password_change: toggles new or existing user flag
246 246 for password change
247 247 :param allow_to_create_user: Defines if the method can actually create
248 248 new users
249 249 :param create_repo_group: Defines if the method should also
250 250 create an repo group with user name, and owner
251 251 :param updating_user_id: if we set it up this is the user we want to
252 252 update this allows to editing username.
253 253 :param language: language of user from interface.
254 254 :param description: user description
255 255 :param strict_creation_check: checks for allowed creation license wise etc.
256 256
257 257 :returns: new User object with injected `is_new_user` attribute.
258 258 """
259 259
260 260 if not cur_user:
261 261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
262 262
263 263 from rhodecode.lib.auth import (
264 264 get_crypt_password, check_password)
265 from rhodecode.lib.hooks_base import (
266 log_create_user, check_allowed_create_user)
265 from rhodecode.lib import hooks_base
267 266
268 267 def _password_change(new_user, password):
269 268 old_password = new_user.password or ''
270 269 # empty password
271 270 if not old_password:
272 271 return False
273 272
274 273 # password check is only needed for RhodeCode internal auth calls
275 274 # in case it's a plugin we don't care
276 275 if not plugin:
277 276
278 277 # first check if we gave crypted password back, and if it
279 278 # matches it's not password change
280 279 if new_user.password == password:
281 280 return False
282 281
283 282 password_match = check_password(password, old_password)
284 283 if not password_match:
285 284 return True
286 285
287 286 return False
288 287
289 288 # read settings on default personal repo group creation
290 289 if create_repo_group is None:
291 290 default_create_repo_group = RepoGroupModel()\
292 291 .get_default_create_personal_repo_group()
293 292 create_repo_group = default_create_repo_group
294 293
295 294 user_data = {
296 295 'username': username,
297 296 'password': password,
298 297 'email': email,
299 298 'firstname': firstname,
300 299 'lastname': lastname,
301 300 'active': active,
302 301 'admin': admin
303 302 }
304 303
305 304 if updating_user_id:
306 305 log.debug('Checking for existing account in RhodeCode '
307 306 'database with user_id `%s` ', updating_user_id)
308 307 user = User.get(updating_user_id)
309 308 else:
310 309 log.debug('Checking for existing account in RhodeCode '
311 310 'database with username `%s` ', username)
312 311 user = User.get_by_username(username, case_insensitive=True)
313 312
314 313 if user is None:
315 314 # we check internal flag if this method is actually allowed to
316 315 # create new user
317 316 if not allow_to_create_user:
318 317 msg = ('Method wants to create new user, but it is not '
319 318 'allowed to do so')
320 319 log.warning(msg)
321 320 raise NotAllowedToCreateUserError(msg)
322 321
323 322 log.debug('Creating new user %s', username)
324 323
325 324 # only if we create user that is active
326 325 new_active_user = active
327 326 if new_active_user and strict_creation_check:
328 327 # raises UserCreationError if it's not allowed for any reason to
329 328 # create new active user, this also executes pre-create hooks
330 check_allowed_create_user(user_data, cur_user, strict_check=True)
329 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
331 330 events.trigger(events.UserPreCreate(user_data))
332 331 new_user = User()
333 332 edit = False
334 333 else:
335 334 log.debug('updating user `%s`', username)
336 335 events.trigger(events.UserPreUpdate(user, user_data))
337 336 new_user = user
338 337 edit = True
339 338
340 339 # we're not allowed to edit default user
341 340 if user.username == User.DEFAULT_USER:
342 341 raise DefaultUserException(
343 342 "You can't edit this user (`%(username)s`) since it's "
344 343 "crucial for entire application"
345 344 % {'username': user.username})
346 345
347 346 # inject special attribute that will tell us if User is new or old
348 347 new_user.is_new_user = not edit
349 348 # for users that didn's specify auth type, we use RhodeCode built in
350 349 from rhodecode.authentication.plugins import auth_rhodecode
351 350 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
352 351 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
353 352
354 353 try:
355 354 new_user.username = username
356 355 new_user.admin = admin
357 356 new_user.email = email
358 357 new_user.active = active
359 358 new_user.extern_name = safe_unicode(extern_name)
360 359 new_user.extern_type = safe_unicode(extern_type)
361 360 new_user.name = firstname
362 361 new_user.lastname = lastname
363 362 new_user.description = description
364 363
365 364 # set password only if creating an user or password is changed
366 365 if not edit or _password_change(new_user, password):
367 366 reason = 'new password' if edit else 'new user'
368 367 log.debug('Updating password reason=>%s', reason)
369 368 new_user.password = get_crypt_password(password) if password else None
370 369
371 370 if force_password_change:
372 371 new_user.update_userdata(force_password_change=True)
373 372 if language:
374 373 new_user.update_userdata(language=language)
375 374 new_user.update_userdata(notification_status=True)
376 375
377 376 self.sa.add(new_user)
378 377
379 378 if not edit and create_repo_group:
380 379 RepoGroupModel().create_personal_repo_group(
381 380 new_user, commit_early=False)
382 381
383 382 if not edit:
384 383 # add the RSS token
385 384 self.add_auth_token(
386 385 user=username, lifetime_minutes=-1,
387 386 role=self.auth_token_role.ROLE_FEED,
388 387 description=u'Generated feed token')
389 388
390 389 kwargs = new_user.get_dict()
391 390 # backward compat, require api_keys present
392 391 kwargs['api_keys'] = kwargs['auth_tokens']
393 log_create_user(created_by=cur_user, **kwargs)
392 hooks_base.create_user(created_by=cur_user, **kwargs)
394 393 events.trigger(events.UserPostCreate(user_data))
395 394 return new_user
396 395 except (DatabaseError,):
397 396 log.error(traceback.format_exc())
398 397 raise
399 398
400 399 def create_registration(self, form_data,
401 400 extern_name='rhodecode', extern_type='rhodecode'):
402 401 from rhodecode.model.notification import NotificationModel
403 402 from rhodecode.model.notification import EmailNotificationModel
404 403
405 404 try:
406 405 form_data['admin'] = False
407 406 form_data['extern_name'] = extern_name
408 407 form_data['extern_type'] = extern_type
409 408 new_user = self.create(form_data)
410 409
411 410 self.sa.add(new_user)
412 411 self.sa.flush()
413 412
414 413 user_data = new_user.get_dict()
415 414 user_data.update({
416 415 'first_name': user_data.get('firstname'),
417 416 'last_name': user_data.get('lastname'),
418 417 })
419 418 kwargs = {
420 419 # use SQLALCHEMY safe dump of user data
421 420 'user': AttributeDict(user_data),
422 421 'date': datetime.datetime.now()
423 422 }
424 423 notification_type = EmailNotificationModel.TYPE_REGISTRATION
425 424 # pre-generate the subject for notification itself
426 425 (subject,
427 426 _h, _e, # we don't care about those
428 427 body_plaintext) = EmailNotificationModel().render_email(
429 428 notification_type, **kwargs)
430 429
431 430 # create notification objects, and emails
432 431 NotificationModel().create(
433 432 created_by=new_user,
434 433 notification_subject=subject,
435 434 notification_body=body_plaintext,
436 435 notification_type=notification_type,
437 436 recipients=None, # all admins
438 437 email_kwargs=kwargs,
439 438 )
440 439
441 440 return new_user
442 441 except Exception:
443 442 log.error(traceback.format_exc())
444 443 raise
445 444
446 445 def _handle_user_repos(self, username, repositories, handle_user,
447 446 handle_mode=None):
448 447
449 448 left_overs = True
450 449
451 450 from rhodecode.model.repo import RepoModel
452 451
453 452 if handle_mode == 'detach':
454 453 for obj in repositories:
455 454 obj.user = handle_user
456 455 # set description we know why we super admin now owns
457 456 # additional repositories that were orphaned !
458 457 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
459 458 self.sa.add(obj)
460 459 left_overs = False
461 460 elif handle_mode == 'delete':
462 461 for obj in repositories:
463 462 RepoModel().delete(obj, forks='detach')
464 463 left_overs = False
465 464
466 465 # if nothing is done we have left overs left
467 466 return left_overs
468 467
469 468 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
470 469 handle_mode=None):
471 470
472 471 left_overs = True
473 472
474 473 from rhodecode.model.repo_group import RepoGroupModel
475 474
476 475 if handle_mode == 'detach':
477 476 for r in repository_groups:
478 477 r.user = handle_user
479 478 # set description we know why we super admin now owns
480 479 # additional repositories that were orphaned !
481 480 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
482 481 r.personal = False
483 482 self.sa.add(r)
484 483 left_overs = False
485 484 elif handle_mode == 'delete':
486 485 for r in repository_groups:
487 486 RepoGroupModel().delete(r)
488 487 left_overs = False
489 488
490 489 # if nothing is done we have left overs left
491 490 return left_overs
492 491
493 492 def _handle_user_user_groups(self, username, user_groups, handle_user,
494 493 handle_mode=None):
495 494
496 495 left_overs = True
497 496
498 497 from rhodecode.model.user_group import UserGroupModel
499 498
500 499 if handle_mode == 'detach':
501 500 for r in user_groups:
502 501 for user_user_group_to_perm in r.user_user_group_to_perm:
503 502 if user_user_group_to_perm.user.username == username:
504 503 user_user_group_to_perm.user = handle_user
505 504 r.user = handle_user
506 505 # set description we know why we super admin now owns
507 506 # additional repositories that were orphaned !
508 507 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
509 508 self.sa.add(r)
510 509 left_overs = False
511 510 elif handle_mode == 'delete':
512 511 for r in user_groups:
513 512 UserGroupModel().delete(r)
514 513 left_overs = False
515 514
516 515 # if nothing is done we have left overs left
517 516 return left_overs
518 517
519 518 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
520 519 handle_mode=None):
521 520 left_overs = True
522 521
523 522 from rhodecode.model.pull_request import PullRequestModel
524 523
525 524 if handle_mode == 'detach':
526 525 for pr in pull_requests:
527 526 pr.user_id = handle_user.user_id
528 527 # set description we know why we super admin now owns
529 528 # additional repositories that were orphaned !
530 529 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
531 530 self.sa.add(pr)
532 531 left_overs = False
533 532 elif handle_mode == 'delete':
534 533 for pr in pull_requests:
535 534 PullRequestModel().delete(pr)
536 535
537 536 left_overs = False
538 537
539 538 # if nothing is done we have left overs left
540 539 return left_overs
541 540
542 541 def _handle_user_artifacts(self, username, artifacts, handle_user,
543 542 handle_mode=None):
544 543
545 544 left_overs = True
546 545
547 546 if handle_mode == 'detach':
548 547 for a in artifacts:
549 548 a.upload_user = handle_user
550 549 # set description we know why we super admin now owns
551 550 # additional artifacts that were orphaned !
552 551 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
553 552 self.sa.add(a)
554 553 left_overs = False
555 554 elif handle_mode == 'delete':
556 555 from rhodecode.apps.file_store import utils as store_utils
557 556 request = get_current_request()
558 557 storage = store_utils.get_file_storage(request.registry.settings)
559 558 for a in artifacts:
560 559 file_uid = a.file_uid
561 560 storage.delete(file_uid)
562 561 self.sa.delete(a)
563 562
564 563 left_overs = False
565 564
566 565 # if nothing is done we have left overs left
567 566 return left_overs
568 567
569 568 def delete(self, user, cur_user=None, handle_repos=None,
570 569 handle_repo_groups=None, handle_user_groups=None,
571 570 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
572 from rhodecode.lib.hooks_base import log_delete_user
571 from rhodecode.lib import hooks_base
573 572
574 573 if not cur_user:
575 574 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
576 575
577 576 user = self._get_user(user)
578 577
579 578 try:
580 579 if user.username == User.DEFAULT_USER:
581 580 raise DefaultUserException(
582 581 u"You can't remove this user since it's"
583 582 u" crucial for entire application")
584 583 handle_user = handle_new_owner or self.cls.get_first_super_admin()
585 584 log.debug('New detached objects owner %s', handle_user)
586 585
587 586 left_overs = self._handle_user_repos(
588 587 user.username, user.repositories, handle_user, handle_repos)
589 588 if left_overs and user.repositories:
590 589 repos = [x.repo_name for x in user.repositories]
591 590 raise UserOwnsReposException(
592 591 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
593 592 u'removed. Switch owners or remove those repositories:%(list_repos)s'
594 593 % {'username': user.username, 'len_repos': len(repos),
595 594 'list_repos': ', '.join(repos)})
596 595
597 596 left_overs = self._handle_user_repo_groups(
598 597 user.username, user.repository_groups, handle_user, handle_repo_groups)
599 598 if left_overs and user.repository_groups:
600 599 repo_groups = [x.group_name for x in user.repository_groups]
601 600 raise UserOwnsRepoGroupsException(
602 601 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
603 602 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
604 603 % {'username': user.username, 'len_repo_groups': len(repo_groups),
605 604 'list_repo_groups': ', '.join(repo_groups)})
606 605
607 606 left_overs = self._handle_user_user_groups(
608 607 user.username, user.user_groups, handle_user, handle_user_groups)
609 608 if left_overs and user.user_groups:
610 609 user_groups = [x.users_group_name for x in user.user_groups]
611 610 raise UserOwnsUserGroupsException(
612 611 u'user "%s" still owns %s user groups and cannot be '
613 612 u'removed. Switch owners or remove those user groups:%s'
614 613 % (user.username, len(user_groups), ', '.join(user_groups)))
615 614
616 615 left_overs = self._handle_user_pull_requests(
617 616 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
618 617 if left_overs and user.user_pull_requests:
619 618 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
620 619 raise UserOwnsPullRequestsException(
621 620 u'user "%s" still owns %s pull requests and cannot be '
622 621 u'removed. Switch owners or remove those pull requests:%s'
623 622 % (user.username, len(pull_requests), ', '.join(pull_requests)))
624 623
625 624 left_overs = self._handle_user_artifacts(
626 625 user.username, user.artifacts, handle_user, handle_artifacts)
627 626 if left_overs and user.artifacts:
628 627 artifacts = [x.file_uid for x in user.artifacts]
629 628 raise UserOwnsArtifactsException(
630 629 u'user "%s" still owns %s artifacts and cannot be '
631 630 u'removed. Switch owners or remove those artifacts:%s'
632 631 % (user.username, len(artifacts), ', '.join(artifacts)))
633 632
634 633 user_data = user.get_dict() # fetch user data before expire
635 634
636 635 # we might change the user data with detach/delete, make sure
637 636 # the object is marked as expired before actually deleting !
638 637 self.sa.expire(user)
639 638 self.sa.delete(user)
640 639
641 log_delete_user(deleted_by=cur_user, **user_data)
640 hooks_base.delete_user(deleted_by=cur_user, **user_data)
642 641 except Exception:
643 642 log.error(traceback.format_exc())
644 643 raise
645 644
646 645 def reset_password_link(self, data, pwd_reset_url):
647 646 from rhodecode.lib.celerylib import tasks, run_task
648 647 from rhodecode.model.notification import EmailNotificationModel
649 648 user_email = data['email']
650 649 try:
651 650 user = User.get_by_email(user_email)
652 651 if user:
653 652 log.debug('password reset user found %s', user)
654 653
655 654 email_kwargs = {
656 655 'password_reset_url': pwd_reset_url,
657 656 'user': user,
658 657 'email': user_email,
659 658 'date': datetime.datetime.now(),
660 659 'first_admin_email': User.get_first_super_admin().email
661 660 }
662 661
663 662 (subject, headers, email_body,
664 663 email_body_plaintext) = EmailNotificationModel().render_email(
665 664 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666 665
667 666 recipients = [user_email]
668 667
669 668 action_logger_generic(
670 669 'sending password reset email to user: {}'.format(
671 670 user), namespace='security.password_reset')
672 671
673 672 run_task(tasks.send_email, recipients, subject,
674 673 email_body_plaintext, email_body)
675 674
676 675 else:
677 676 log.debug("password reset email %s not found", user_email)
678 677 except Exception:
679 678 log.error(traceback.format_exc())
680 679 return False
681 680
682 681 return True
683 682
684 683 def reset_password(self, data):
685 684 from rhodecode.lib.celerylib import tasks, run_task
686 685 from rhodecode.model.notification import EmailNotificationModel
687 686 from rhodecode.lib import auth
688 687 user_email = data['email']
689 688 pre_db = True
690 689 try:
691 690 user = User.get_by_email(user_email)
692 691 new_passwd = auth.PasswordGenerator().gen_password(
693 692 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
694 693 if user:
695 694 user.password = auth.get_crypt_password(new_passwd)
696 695 # also force this user to reset his password !
697 696 user.update_userdata(force_password_change=True)
698 697
699 698 Session().add(user)
700 699
701 700 # now delete the token in question
702 701 UserApiKeys = AuthTokenModel.cls
703 702 UserApiKeys().query().filter(
704 703 UserApiKeys.api_key == data['token']).delete()
705 704
706 705 Session().commit()
707 706 log.info('successfully reset password for `%s`', user_email)
708 707
709 708 if new_passwd is None:
710 709 raise Exception('unable to generate new password')
711 710
712 711 pre_db = False
713 712
714 713 email_kwargs = {
715 714 'new_password': new_passwd,
716 715 'user': user,
717 716 'email': user_email,
718 717 'date': datetime.datetime.now(),
719 718 'first_admin_email': User.get_first_super_admin().email
720 719 }
721 720
722 721 (subject, headers, email_body,
723 722 email_body_plaintext) = EmailNotificationModel().render_email(
724 723 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
725 724 **email_kwargs)
726 725
727 726 recipients = [user_email]
728 727
729 728 action_logger_generic(
730 729 'sent new password to user: {} with email: {}'.format(
731 730 user, user_email), namespace='security.password_reset')
732 731
733 732 run_task(tasks.send_email, recipients, subject,
734 733 email_body_plaintext, email_body)
735 734
736 735 except Exception:
737 736 log.error('Failed to update user password')
738 737 log.error(traceback.format_exc())
739 738 if pre_db:
740 739 # we rollback only if local db stuff fails. If it goes into
741 740 # run_task, we're pass rollback state this wouldn't work then
742 741 Session().rollback()
743 742
744 743 return True
745 744
746 745 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
747 746 """
748 747 Fetches auth_user by user_id,or api_key if present.
749 748 Fills auth_user attributes with those taken from database.
750 749 Additionally set's is_authenitated if lookup fails
751 750 present in database
752 751
753 752 :param auth_user: instance of user to set attributes
754 753 :param user_id: user id to fetch by
755 754 :param api_key: api key to fetch by
756 755 :param username: username to fetch by
757 756 """
758 757 def token_obfuscate(token):
759 758 if token:
760 759 return token[:4] + "****"
761 760
762 761 if user_id is None and api_key is None and username is None:
763 762 raise Exception('You need to pass user_id, api_key or username')
764 763
765 764 log.debug(
766 765 'AuthUser: fill data execution based on: '
767 766 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
768 767 try:
769 768 dbuser = None
770 769 if user_id:
771 770 dbuser = self.get(user_id)
772 771 elif api_key:
773 772 dbuser = self.get_by_auth_token(api_key)
774 773 elif username:
775 774 dbuser = self.get_by_username(username)
776 775
777 776 if not dbuser:
778 777 log.warning(
779 778 'Unable to lookup user by id:%s api_key:%s username:%s',
780 779 user_id, token_obfuscate(api_key), username)
781 780 return False
782 781 if not dbuser.active:
783 782 log.debug('User `%s:%s` is inactive, skipping fill data',
784 783 username, user_id)
785 784 return False
786 785
787 786 log.debug('AuthUser: filling found user:%s data', dbuser)
788 787
789 788 attrs = {
790 789 'user_id': dbuser.user_id,
791 790 'username': dbuser.username,
792 791 'name': dbuser.name,
793 792 'first_name': dbuser.first_name,
794 793 'firstname': dbuser.firstname,
795 794 'last_name': dbuser.last_name,
796 795 'lastname': dbuser.lastname,
797 796 'admin': dbuser.admin,
798 797 'active': dbuser.active,
799 798
800 799 'email': dbuser.email,
801 800 'emails': dbuser.emails_cached(),
802 801 'short_contact': dbuser.short_contact,
803 802 'full_contact': dbuser.full_contact,
804 803 'full_name': dbuser.full_name,
805 804 'full_name_or_username': dbuser.full_name_or_username,
806 805
807 806 '_api_key': dbuser._api_key,
808 807 '_user_data': dbuser._user_data,
809 808
810 809 'created_on': dbuser.created_on,
811 810 'extern_name': dbuser.extern_name,
812 811 'extern_type': dbuser.extern_type,
813 812
814 813 'inherit_default_permissions': dbuser.inherit_default_permissions,
815 814
816 815 'language': dbuser.language,
817 816 'last_activity': dbuser.last_activity,
818 817 'last_login': dbuser.last_login,
819 818 'password': dbuser.password,
820 819 }
821 820 auth_user.__dict__.update(attrs)
822 821 except Exception:
823 822 log.error(traceback.format_exc())
824 823 auth_user.is_authenticated = False
825 824 return False
826 825
827 826 return True
828 827
829 828 def has_perm(self, user, perm):
830 829 perm = self._get_perm(perm)
831 830 user = self._get_user(user)
832 831
833 832 return UserToPerm.query().filter(UserToPerm.user == user)\
834 833 .filter(UserToPerm.permission == perm).scalar() is not None
835 834
836 835 def grant_perm(self, user, perm):
837 836 """
838 837 Grant user global permissions
839 838
840 839 :param user:
841 840 :param perm:
842 841 """
843 842 user = self._get_user(user)
844 843 perm = self._get_perm(perm)
845 844 # if this permission is already granted skip it
846 845 _perm = UserToPerm.query()\
847 846 .filter(UserToPerm.user == user)\
848 847 .filter(UserToPerm.permission == perm)\
849 848 .scalar()
850 849 if _perm:
851 850 return
852 851 new = UserToPerm()
853 852 new.user = user
854 853 new.permission = perm
855 854 self.sa.add(new)
856 855 return new
857 856
858 857 def revoke_perm(self, user, perm):
859 858 """
860 859 Revoke users global permissions
861 860
862 861 :param user:
863 862 :param perm:
864 863 """
865 864 user = self._get_user(user)
866 865 perm = self._get_perm(perm)
867 866
868 867 obj = UserToPerm.query()\
869 868 .filter(UserToPerm.user == user)\
870 869 .filter(UserToPerm.permission == perm)\
871 870 .scalar()
872 871 if obj:
873 872 self.sa.delete(obj)
874 873
875 874 def add_extra_email(self, user, email):
876 875 """
877 876 Adds email address to UserEmailMap
878 877
879 878 :param user:
880 879 :param email:
881 880 """
882 881
883 882 user = self._get_user(user)
884 883
885 884 obj = UserEmailMap()
886 885 obj.user = user
887 886 obj.email = email
888 887 self.sa.add(obj)
889 888 return obj
890 889
891 890 def delete_extra_email(self, user, email_id):
892 891 """
893 892 Removes email address from UserEmailMap
894 893
895 894 :param user:
896 895 :param email_id:
897 896 """
898 897 user = self._get_user(user)
899 898 obj = UserEmailMap.query().get(email_id)
900 899 if obj and obj.user_id == user.user_id:
901 900 self.sa.delete(obj)
902 901
903 902 def parse_ip_range(self, ip_range):
904 903 ip_list = []
905 904
906 905 def make_unique(value):
907 906 seen = []
908 907 return [c for c in value if not (c in seen or seen.append(c))]
909 908
910 909 # firsts split by commas
911 910 for ip_range in ip_range.split(','):
912 911 if not ip_range:
913 912 continue
914 913 ip_range = ip_range.strip()
915 914 if '-' in ip_range:
916 915 start_ip, end_ip = ip_range.split('-', 1)
917 916 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
918 917 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
919 918 parsed_ip_range = []
920 919
921 920 for index in range(int(start_ip), int(end_ip) + 1):
922 921 new_ip = ipaddress.ip_address(index)
923 922 parsed_ip_range.append(str(new_ip))
924 923 ip_list.extend(parsed_ip_range)
925 924 else:
926 925 ip_list.append(ip_range)
927 926
928 927 return make_unique(ip_list)
929 928
930 929 def add_extra_ip(self, user, ip, description=None):
931 930 """
932 931 Adds ip address to UserIpMap
933 932
934 933 :param user:
935 934 :param ip:
936 935 """
937 936
938 937 user = self._get_user(user)
939 938 obj = UserIpMap()
940 939 obj.user = user
941 940 obj.ip_addr = ip
942 941 obj.description = description
943 942 self.sa.add(obj)
944 943 return obj
945 944
946 945 auth_token_role = AuthTokenModel.cls
947 946
948 947 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
949 948 scope_callback=None):
950 949 """
951 950 Add AuthToken for user.
952 951
953 952 :param user: username/user_id
954 953 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
955 954 :param role: one of AuthTokenModel.cls.ROLE_*
956 955 :param description: optional string description
957 956 """
958 957
959 958 token = AuthTokenModel().create(
960 959 user, description, lifetime_minutes, role)
961 960 if scope_callback and callable(scope_callback):
962 961 # call the callback if we provide, used to attach scope for EE edition
963 962 scope_callback(token)
964 963 return token
965 964
966 965 def delete_extra_ip(self, user, ip_id):
967 966 """
968 967 Removes ip address from UserIpMap
969 968
970 969 :param user:
971 970 :param ip_id:
972 971 """
973 972 user = self._get_user(user)
974 973 obj = UserIpMap.query().get(ip_id)
975 974 if obj and obj.user_id == user.user_id:
976 975 self.sa.delete(obj)
977 976
978 977 def get_accounts_in_creation_order(self, current_user=None):
979 978 """
980 979 Get accounts in order of creation for deactivation for license limits
981 980
982 981 pick currently logged in user, and append to the list in position 0
983 982 pick all super-admins in order of creation date and add it to the list
984 983 pick all other accounts in order of creation and add it to the list.
985 984
986 985 Based on that list, the last accounts can be disabled as they are
987 986 created at the end and don't include any of the super admins as well
988 987 as the current user.
989 988
990 989 :param current_user: optionally current user running this operation
991 990 """
992 991
993 992 if not current_user:
994 993 current_user = get_current_rhodecode_user()
995 994 active_super_admins = [
996 995 x.user_id for x in User.query()
997 996 .filter(User.user_id != current_user.user_id)
998 997 .filter(User.active == true())
999 998 .filter(User.admin == true())
1000 999 .order_by(User.created_on.asc())]
1001 1000
1002 1001 active_regular_users = [
1003 1002 x.user_id for x in User.query()
1004 1003 .filter(User.user_id != current_user.user_id)
1005 1004 .filter(User.active == true())
1006 1005 .filter(User.admin == false())
1007 1006 .order_by(User.created_on.asc())]
1008 1007
1009 1008 list_of_accounts = [current_user.user_id]
1010 1009 list_of_accounts += active_super_admins
1011 1010 list_of_accounts += active_regular_users
1012 1011
1013 1012 return list_of_accounts
1014 1013
1015 1014 def deactivate_last_users(self, expected_users, current_user=None):
1016 1015 """
1017 1016 Deactivate accounts that are over the license limits.
1018 1017 Algorithm of which accounts to disabled is based on the formula:
1019 1018
1020 1019 Get current user, then super admins in creation order, then regular
1021 1020 active users in creation order.
1022 1021
1023 1022 Using that list we mark all accounts from the end of it as inactive.
1024 1023 This way we block only latest created accounts.
1025 1024
1026 1025 :param expected_users: list of users in special order, we deactivate
1027 1026 the end N amount of users from that list
1028 1027 """
1029 1028
1030 1029 list_of_accounts = self.get_accounts_in_creation_order(
1031 1030 current_user=current_user)
1032 1031
1033 1032 for acc_id in list_of_accounts[expected_users + 1:]:
1034 1033 user = User.get(acc_id)
1035 1034 log.info('Deactivating account %s for license unlock', user)
1036 1035 user.active = False
1037 1036 Session().add(user)
1038 1037 Session().commit()
1039 1038
1040 1039 return
1041 1040
1042 1041 def get_user_log(self, user, filter_term):
1043 1042 user_log = UserLog.query()\
1044 1043 .filter(or_(UserLog.user_id == user.user_id,
1045 1044 UserLog.username == user.username))\
1046 1045 .options(joinedload(UserLog.user))\
1047 1046 .options(joinedload(UserLog.repository))\
1048 1047 .order_by(UserLog.action_date.desc())
1049 1048
1050 1049 user_log = user_log_filter(user_log, filter_term)
1051 1050 return user_log
General Comments 0
You need to be logged in to leave comments. Login now