##// END OF EJS Templates
api: added proper full permission flush on API calls when creating repos and repo groups....
super-admin -
r4697:63f392fc stable
parent child Browse files
Show More
@@ -1,759 +1,762 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 import logging
23 23
24 24 from rhodecode.api import JSONRPCValidationError
25 25 from rhodecode.api import jsonrpc_method, JSONRPCError
26 26 from rhodecode.api.utils import (
27 27 has_superadmin_permission, Optional, OAttr, get_user_or_error,
28 28 get_repo_group_or_error, get_perm_or_error, get_user_group_or_error,
29 29 get_origin, validate_repo_group_permissions, validate_set_owner_permissions)
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
33 33 from rhodecode.model.db import Session
34 34 from rhodecode.model.permission import PermissionModel
35 35 from rhodecode.model.repo_group import RepoGroupModel
36 36 from rhodecode.model.scm import RepoGroupList
37 37 from rhodecode.model import validation_schema
38 38 from rhodecode.model.validation_schema.schemas import repo_group_schema
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 @jsonrpc_method()
45 45 def get_repo_group(request, apiuser, repogroupid):
46 46 """
47 47 Return the specified |repo| group, along with permissions,
48 48 and repositories inside the group
49 49
50 50 :param apiuser: This is filled automatically from the |authtoken|.
51 51 :type apiuser: AuthUser
52 52 :param repogroupid: Specify the name of ID of the repository group.
53 53 :type repogroupid: str or int
54 54
55 55
56 56 Example output:
57 57
58 58 .. code-block:: bash
59 59
60 60 {
61 61 "error": null,
62 62 "id": repo-group-id,
63 63 "result": {
64 64 "group_description": "repo group description",
65 65 "group_id": 14,
66 66 "group_name": "group name",
67 67 "permissions": [
68 68 {
69 69 "name": "super-admin-username",
70 70 "origin": "super-admin",
71 71 "permission": "group.admin",
72 72 "type": "user"
73 73 },
74 74 {
75 75 "name": "owner-name",
76 76 "origin": "owner",
77 77 "permission": "group.admin",
78 78 "type": "user"
79 79 },
80 80 {
81 81 "name": "user-group-name",
82 82 "origin": "permission",
83 83 "permission": "group.write",
84 84 "type": "user_group"
85 85 }
86 86 ],
87 87 "owner": "owner-name",
88 88 "parent_group": null,
89 89 "repositories": [ repo-list ]
90 90 }
91 91 }
92 92 """
93 93
94 94 repo_group = get_repo_group_or_error(repogroupid)
95 95 if not has_superadmin_permission(apiuser):
96 96 # check if we have at least read permission for this repo group !
97 97 _perms = ('group.admin', 'group.write', 'group.read',)
98 98 if not HasRepoGroupPermissionAnyApi(*_perms)(
99 99 user=apiuser, group_name=repo_group.group_name):
100 100 raise JSONRPCError(
101 101 'repository group `%s` does not exist' % (repogroupid,))
102 102
103 103 permissions = []
104 104 for _user in repo_group.permissions():
105 105 user_data = {
106 106 'name': _user.username,
107 107 'permission': _user.permission,
108 108 'origin': get_origin(_user),
109 109 'type': "user",
110 110 }
111 111 permissions.append(user_data)
112 112
113 113 for _user_group in repo_group.permission_user_groups():
114 114 user_group_data = {
115 115 'name': _user_group.users_group_name,
116 116 'permission': _user_group.permission,
117 117 'origin': get_origin(_user_group),
118 118 'type': "user_group",
119 119 }
120 120 permissions.append(user_group_data)
121 121
122 122 data = repo_group.get_api_data()
123 123 data["permissions"] = permissions
124 124 return data
125 125
126 126
127 127 @jsonrpc_method()
128 128 def get_repo_groups(request, apiuser):
129 129 """
130 130 Returns all repository groups.
131 131
132 132 :param apiuser: This is filled automatically from the |authtoken|.
133 133 :type apiuser: AuthUser
134 134 """
135 135
136 136 result = []
137 137 _perms = ('group.read', 'group.write', 'group.admin',)
138 138 extras = {'user': apiuser}
139 139 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
140 140 perm_set=_perms, extra_kwargs=extras):
141 141 result.append(repo_group.get_api_data())
142 142 return result
143 143
144 144
145 145 @jsonrpc_method()
146 146 def create_repo_group(
147 147 request, apiuser, group_name,
148 148 owner=Optional(OAttr('apiuser')),
149 149 description=Optional(''),
150 150 copy_permissions=Optional(False)):
151 151 """
152 152 Creates a repository group.
153 153
154 154 * If the repository group name contains "/", repository group will be
155 155 created inside a repository group or nested repository groups
156 156
157 157 For example "foo/bar/group1" will create repository group called "group1"
158 158 inside group "foo/bar". You have to have permissions to access and
159 159 write to the last repository group ("bar" in this example)
160 160
161 161 This command can only be run using an |authtoken| with at least
162 162 permissions to create repository groups, or admin permissions to
163 163 parent repository groups.
164 164
165 165 :param apiuser: This is filled automatically from the |authtoken|.
166 166 :type apiuser: AuthUser
167 167 :param group_name: Set the repository group name.
168 168 :type group_name: str
169 169 :param description: Set the |repo| group description.
170 170 :type description: str
171 171 :param owner: Set the |repo| group owner.
172 172 :type owner: str
173 173 :param copy_permissions:
174 174 :type copy_permissions:
175 175
176 176 Example output:
177 177
178 178 .. code-block:: bash
179 179
180 180 id : <id_given_in_input>
181 181 result : {
182 182 "msg": "Created new repo group `<repo_group_name>`"
183 183 "repo_group": <repogroup_object>
184 184 }
185 185 error : null
186 186
187 187
188 188 Example error output:
189 189
190 190 .. code-block:: bash
191 191
192 192 id : <id_given_in_input>
193 193 result : null
194 194 error : {
195 195 failed to create repo group `<repogroupid>`
196 196 }
197 197
198 198 """
199 199
200 200 owner = validate_set_owner_permissions(apiuser, owner)
201 201
202 202 description = Optional.extract(description)
203 203 copy_permissions = Optional.extract(copy_permissions)
204 204
205 205 schema = repo_group_schema.RepoGroupSchema().bind(
206 206 # user caller
207 207 user=apiuser)
208 208
209 209 try:
210 210 schema_data = schema.deserialize(dict(
211 211 repo_group_name=group_name,
212 212 repo_group_owner=owner.username,
213 213 repo_group_description=description,
214 214 repo_group_copy_permissions=copy_permissions,
215 215 ))
216 216 except validation_schema.Invalid as err:
217 217 raise JSONRPCValidationError(colander_exc=err)
218 218
219 219 validated_group_name = schema_data['repo_group_name']
220 220
221 221 try:
222 222 repo_group = RepoGroupModel().create(
223 223 owner=owner,
224 224 group_name=validated_group_name,
225 225 group_description=schema_data['repo_group_description'],
226 226 copy_permissions=schema_data['repo_group_copy_permissions'])
227 227 Session().flush()
228 228
229 229 repo_group_data = repo_group.get_api_data()
230 230 audit_logger.store_api(
231 231 'repo_group.create', action_data={'data': repo_group_data},
232 232 user=apiuser)
233 233
234 234 Session().commit()
235
236 PermissionModel().trigger_permission_flush()
237
235 238 return {
236 239 'msg': 'Created new repo group `%s`' % validated_group_name,
237 240 'repo_group': repo_group.get_api_data()
238 241 }
239 242 except Exception:
240 243 log.exception("Exception occurred while trying create repo group")
241 244 raise JSONRPCError(
242 245 'failed to create repo group `%s`' % (validated_group_name,))
243 246
244 247
245 248 @jsonrpc_method()
246 249 def update_repo_group(
247 250 request, apiuser, repogroupid, group_name=Optional(''),
248 251 description=Optional(''), owner=Optional(OAttr('apiuser')),
249 252 enable_locking=Optional(False)):
250 253 """
251 254 Updates repository group with the details given.
252 255
253 256 This command can only be run using an |authtoken| with admin
254 257 permissions.
255 258
256 259 * If the group_name name contains "/", repository group will be updated
257 260 accordingly with a repository group or nested repository groups
258 261
259 262 For example repogroupid=group-test group_name="foo/bar/group-test"
260 263 will update repository group called "group-test" and place it
261 264 inside group "foo/bar".
262 265 You have to have permissions to access and write to the last repository
263 266 group ("bar" in this example)
264 267
265 268 :param apiuser: This is filled automatically from the |authtoken|.
266 269 :type apiuser: AuthUser
267 270 :param repogroupid: Set the ID of repository group.
268 271 :type repogroupid: str or int
269 272 :param group_name: Set the name of the |repo| group.
270 273 :type group_name: str
271 274 :param description: Set a description for the group.
272 275 :type description: str
273 276 :param owner: Set the |repo| group owner.
274 277 :type owner: str
275 278 :param enable_locking: Enable |repo| locking. The default is false.
276 279 :type enable_locking: bool
277 280 """
278 281
279 282 repo_group = get_repo_group_or_error(repogroupid)
280 283
281 284 if not has_superadmin_permission(apiuser):
282 285 validate_repo_group_permissions(
283 286 apiuser, repogroupid, repo_group, ('group.admin',))
284 287
285 288 updates = dict(
286 289 group_name=group_name
287 290 if not isinstance(group_name, Optional) else repo_group.group_name,
288 291
289 292 group_description=description
290 293 if not isinstance(description, Optional) else repo_group.group_description,
291 294
292 295 user=owner
293 296 if not isinstance(owner, Optional) else repo_group.user.username,
294 297
295 298 enable_locking=enable_locking
296 299 if not isinstance(enable_locking, Optional) else repo_group.enable_locking
297 300 )
298 301
299 302 schema = repo_group_schema.RepoGroupSchema().bind(
300 303 # user caller
301 304 user=apiuser,
302 305 old_values=repo_group.get_api_data())
303 306
304 307 try:
305 308 schema_data = schema.deserialize(dict(
306 309 repo_group_name=updates['group_name'],
307 310 repo_group_owner=updates['user'],
308 311 repo_group_description=updates['group_description'],
309 312 repo_group_enable_locking=updates['enable_locking'],
310 313 ))
311 314 except validation_schema.Invalid as err:
312 315 raise JSONRPCValidationError(colander_exc=err)
313 316
314 317 validated_updates = dict(
315 318 group_name=schema_data['repo_group']['repo_group_name_without_group'],
316 319 group_parent_id=schema_data['repo_group']['repo_group_id'],
317 320 user=schema_data['repo_group_owner'],
318 321 group_description=schema_data['repo_group_description'],
319 322 enable_locking=schema_data['repo_group_enable_locking'],
320 323 )
321 324
322 325 old_data = repo_group.get_api_data()
323 326 try:
324 327 RepoGroupModel().update(repo_group, validated_updates)
325 328 audit_logger.store_api(
326 329 'repo_group.edit', action_data={'old_data': old_data},
327 330 user=apiuser)
328 331
329 332 Session().commit()
330 333 return {
331 334 'msg': 'updated repository group ID:%s %s' % (
332 335 repo_group.group_id, repo_group.group_name),
333 336 'repo_group': repo_group.get_api_data()
334 337 }
335 338 except Exception:
336 339 log.exception(
337 340 u"Exception occurred while trying update repo group %s",
338 341 repogroupid)
339 342 raise JSONRPCError('failed to update repository group `%s`'
340 343 % (repogroupid,))
341 344
342 345
343 346 @jsonrpc_method()
344 347 def delete_repo_group(request, apiuser, repogroupid):
345 348 """
346 349 Deletes a |repo| group.
347 350
348 351 :param apiuser: This is filled automatically from the |authtoken|.
349 352 :type apiuser: AuthUser
350 353 :param repogroupid: Set the name or ID of repository group to be
351 354 deleted.
352 355 :type repogroupid: str or int
353 356
354 357 Example output:
355 358
356 359 .. code-block:: bash
357 360
358 361 id : <id_given_in_input>
359 362 result : {
360 363 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>'
361 364 'repo_group': null
362 365 }
363 366 error : null
364 367
365 368 Example error output:
366 369
367 370 .. code-block:: bash
368 371
369 372 id : <id_given_in_input>
370 373 result : null
371 374 error : {
372 375 "failed to delete repo group ID:<repogroupid> <repogroupname>"
373 376 }
374 377
375 378 """
376 379
377 380 repo_group = get_repo_group_or_error(repogroupid)
378 381 if not has_superadmin_permission(apiuser):
379 382 validate_repo_group_permissions(
380 383 apiuser, repogroupid, repo_group, ('group.admin',))
381 384
382 385 old_data = repo_group.get_api_data()
383 386 try:
384 387 RepoGroupModel().delete(repo_group)
385 388 audit_logger.store_api(
386 389 'repo_group.delete', action_data={'old_data': old_data},
387 390 user=apiuser)
388 391 Session().commit()
389 392 return {
390 393 'msg': 'deleted repo group ID:%s %s' %
391 394 (repo_group.group_id, repo_group.group_name),
392 395 'repo_group': None
393 396 }
394 397 except Exception:
395 398 log.exception("Exception occurred while trying to delete repo group")
396 399 raise JSONRPCError('failed to delete repo group ID:%s %s' %
397 400 (repo_group.group_id, repo_group.group_name))
398 401
399 402
400 403 @jsonrpc_method()
401 404 def grant_user_permission_to_repo_group(
402 405 request, apiuser, repogroupid, userid, perm,
403 406 apply_to_children=Optional('none')):
404 407 """
405 408 Grant permission for a user on the given repository group, or update
406 409 existing permissions if found.
407 410
408 411 This command can only be run using an |authtoken| with admin
409 412 permissions.
410 413
411 414 :param apiuser: This is filled automatically from the |authtoken|.
412 415 :type apiuser: AuthUser
413 416 :param repogroupid: Set the name or ID of repository group.
414 417 :type repogroupid: str or int
415 418 :param userid: Set the user name.
416 419 :type userid: str
417 420 :param perm: (group.(none|read|write|admin))
418 421 :type perm: str
419 422 :param apply_to_children: 'none', 'repos', 'groups', 'all'
420 423 :type apply_to_children: str
421 424
422 425 Example output:
423 426
424 427 .. code-block:: bash
425 428
426 429 id : <id_given_in_input>
427 430 result: {
428 431 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
429 432 "success": true
430 433 }
431 434 error: null
432 435
433 436 Example error output:
434 437
435 438 .. code-block:: bash
436 439
437 440 id : <id_given_in_input>
438 441 result : null
439 442 error : {
440 443 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
441 444 }
442 445
443 446 """
444 447
445 448 repo_group = get_repo_group_or_error(repogroupid)
446 449
447 450 if not has_superadmin_permission(apiuser):
448 451 validate_repo_group_permissions(
449 452 apiuser, repogroupid, repo_group, ('group.admin',))
450 453
451 454 user = get_user_or_error(userid)
452 455 perm = get_perm_or_error(perm, prefix='group.')
453 456 apply_to_children = Optional.extract(apply_to_children)
454 457
455 458 perm_additions = [[user.user_id, perm.permission_name, "user"]]
456 459 try:
457 460 changes = RepoGroupModel().update_permissions(
458 461 repo_group=repo_group, perm_additions=perm_additions,
459 462 recursive=apply_to_children, cur_user=apiuser)
460 463
461 464 action_data = {
462 465 'added': changes['added'],
463 466 'updated': changes['updated'],
464 467 'deleted': changes['deleted'],
465 468 }
466 469 audit_logger.store_api(
467 470 'repo_group.edit.permissions', action_data=action_data,
468 471 user=apiuser)
469 472 Session().commit()
470 473 PermissionModel().flush_user_permission_caches(changes)
471 474
472 475 return {
473 476 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
474 477 '`%s` in repo group: `%s`' % (
475 478 perm.permission_name, apply_to_children, user.username,
476 479 repo_group.name
477 480 ),
478 481 'success': True
479 482 }
480 483 except Exception:
481 484 log.exception("Exception occurred while trying to grant "
482 485 "user permissions to repo group")
483 486 raise JSONRPCError(
484 487 'failed to edit permission for user: '
485 488 '`%s` in repo group: `%s`' % (userid, repo_group.name))
486 489
487 490
488 491 @jsonrpc_method()
489 492 def revoke_user_permission_from_repo_group(
490 493 request, apiuser, repogroupid, userid,
491 494 apply_to_children=Optional('none')):
492 495 """
493 496 Revoke permission for a user in a given repository group.
494 497
495 498 This command can only be run using an |authtoken| with admin
496 499 permissions on the |repo| group.
497 500
498 501 :param apiuser: This is filled automatically from the |authtoken|.
499 502 :type apiuser: AuthUser
500 503 :param repogroupid: Set the name or ID of the repository group.
501 504 :type repogroupid: str or int
502 505 :param userid: Set the user name to revoke.
503 506 :type userid: str
504 507 :param apply_to_children: 'none', 'repos', 'groups', 'all'
505 508 :type apply_to_children: str
506 509
507 510 Example output:
508 511
509 512 .. code-block:: bash
510 513
511 514 id : <id_given_in_input>
512 515 result: {
513 516 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
514 517 "success": true
515 518 }
516 519 error: null
517 520
518 521 Example error output:
519 522
520 523 .. code-block:: bash
521 524
522 525 id : <id_given_in_input>
523 526 result : null
524 527 error : {
525 528 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
526 529 }
527 530
528 531 """
529 532
530 533 repo_group = get_repo_group_or_error(repogroupid)
531 534
532 535 if not has_superadmin_permission(apiuser):
533 536 validate_repo_group_permissions(
534 537 apiuser, repogroupid, repo_group, ('group.admin',))
535 538
536 539 user = get_user_or_error(userid)
537 540 apply_to_children = Optional.extract(apply_to_children)
538 541
539 542 perm_deletions = [[user.user_id, None, "user"]]
540 543 try:
541 544 changes = RepoGroupModel().update_permissions(
542 545 repo_group=repo_group, perm_deletions=perm_deletions,
543 546 recursive=apply_to_children, cur_user=apiuser)
544 547
545 548 action_data = {
546 549 'added': changes['added'],
547 550 'updated': changes['updated'],
548 551 'deleted': changes['deleted'],
549 552 }
550 553 audit_logger.store_api(
551 554 'repo_group.edit.permissions', action_data=action_data,
552 555 user=apiuser)
553 556 Session().commit()
554 557 PermissionModel().flush_user_permission_caches(changes)
555 558
556 559 return {
557 560 'msg': 'Revoked perm (recursive:%s) for user: '
558 561 '`%s` in repo group: `%s`' % (
559 562 apply_to_children, user.username, repo_group.name
560 563 ),
561 564 'success': True
562 565 }
563 566 except Exception:
564 567 log.exception("Exception occurred while trying revoke user "
565 568 "permission from repo group")
566 569 raise JSONRPCError(
567 570 'failed to edit permission for user: '
568 571 '`%s` in repo group: `%s`' % (userid, repo_group.name))
569 572
570 573
571 574 @jsonrpc_method()
572 575 def grant_user_group_permission_to_repo_group(
573 576 request, apiuser, repogroupid, usergroupid, perm,
574 577 apply_to_children=Optional('none'), ):
575 578 """
576 579 Grant permission for a user group on given repository group, or update
577 580 existing permissions if found.
578 581
579 582 This command can only be run using an |authtoken| with admin
580 583 permissions on the |repo| group.
581 584
582 585 :param apiuser: This is filled automatically from the |authtoken|.
583 586 :type apiuser: AuthUser
584 587 :param repogroupid: Set the name or id of repository group
585 588 :type repogroupid: str or int
586 589 :param usergroupid: id of usergroup
587 590 :type usergroupid: str or int
588 591 :param perm: (group.(none|read|write|admin))
589 592 :type perm: str
590 593 :param apply_to_children: 'none', 'repos', 'groups', 'all'
591 594 :type apply_to_children: str
592 595
593 596 Example output:
594 597
595 598 .. code-block:: bash
596 599
597 600 id : <id_given_in_input>
598 601 result : {
599 602 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
600 603 "success": true
601 604
602 605 }
603 606 error : null
604 607
605 608 Example error output:
606 609
607 610 .. code-block:: bash
608 611
609 612 id : <id_given_in_input>
610 613 result : null
611 614 error : {
612 615 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
613 616 }
614 617
615 618 """
616 619
617 620 repo_group = get_repo_group_or_error(repogroupid)
618 621 perm = get_perm_or_error(perm, prefix='group.')
619 622 user_group = get_user_group_or_error(usergroupid)
620 623 if not has_superadmin_permission(apiuser):
621 624 validate_repo_group_permissions(
622 625 apiuser, repogroupid, repo_group, ('group.admin',))
623 626
624 627 # check if we have at least read permission for this user group !
625 628 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
626 629 if not HasUserGroupPermissionAnyApi(*_perms)(
627 630 user=apiuser, user_group_name=user_group.users_group_name):
628 631 raise JSONRPCError(
629 632 'user group `%s` does not exist' % (usergroupid,))
630 633
631 634 apply_to_children = Optional.extract(apply_to_children)
632 635
633 636 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
634 637 try:
635 638 changes = RepoGroupModel().update_permissions(
636 639 repo_group=repo_group, perm_additions=perm_additions,
637 640 recursive=apply_to_children, cur_user=apiuser)
638 641
639 642 action_data = {
640 643 'added': changes['added'],
641 644 'updated': changes['updated'],
642 645 'deleted': changes['deleted'],
643 646 }
644 647 audit_logger.store_api(
645 648 'repo_group.edit.permissions', action_data=action_data,
646 649 user=apiuser)
647 650 Session().commit()
648 651 PermissionModel().flush_user_permission_caches(changes)
649 652
650 653 return {
651 654 'msg': 'Granted perm: `%s` (recursive:%s) '
652 655 'for user group: `%s` in repo group: `%s`' % (
653 656 perm.permission_name, apply_to_children,
654 657 user_group.users_group_name, repo_group.name
655 658 ),
656 659 'success': True
657 660 }
658 661 except Exception:
659 662 log.exception("Exception occurred while trying to grant user "
660 663 "group permissions to repo group")
661 664 raise JSONRPCError(
662 665 'failed to edit permission for user group: `%s` in '
663 666 'repo group: `%s`' % (
664 667 usergroupid, repo_group.name
665 668 )
666 669 )
667 670
668 671
669 672 @jsonrpc_method()
670 673 def revoke_user_group_permission_from_repo_group(
671 674 request, apiuser, repogroupid, usergroupid,
672 675 apply_to_children=Optional('none')):
673 676 """
674 677 Revoke permission for user group on given repository.
675 678
676 679 This command can only be run using an |authtoken| with admin
677 680 permissions on the |repo| group.
678 681
679 682 :param apiuser: This is filled automatically from the |authtoken|.
680 683 :type apiuser: AuthUser
681 684 :param repogroupid: name or id of repository group
682 685 :type repogroupid: str or int
683 686 :param usergroupid:
684 687 :param apply_to_children: 'none', 'repos', 'groups', 'all'
685 688 :type apply_to_children: str
686 689
687 690 Example output:
688 691
689 692 .. code-block:: bash
690 693
691 694 id : <id_given_in_input>
692 695 result: {
693 696 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
694 697 "success": true
695 698 }
696 699 error: null
697 700
698 701 Example error output:
699 702
700 703 .. code-block:: bash
701 704
702 705 id : <id_given_in_input>
703 706 result : null
704 707 error : {
705 708 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
706 709 }
707 710
708 711
709 712 """
710 713
711 714 repo_group = get_repo_group_or_error(repogroupid)
712 715 user_group = get_user_group_or_error(usergroupid)
713 716 if not has_superadmin_permission(apiuser):
714 717 validate_repo_group_permissions(
715 718 apiuser, repogroupid, repo_group, ('group.admin',))
716 719
717 720 # check if we have at least read permission for this user group !
718 721 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
719 722 if not HasUserGroupPermissionAnyApi(*_perms)(
720 723 user=apiuser, user_group_name=user_group.users_group_name):
721 724 raise JSONRPCError(
722 725 'user group `%s` does not exist' % (usergroupid,))
723 726
724 727 apply_to_children = Optional.extract(apply_to_children)
725 728
726 729 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
727 730 try:
728 731 changes = RepoGroupModel().update_permissions(
729 732 repo_group=repo_group, perm_deletions=perm_deletions,
730 733 recursive=apply_to_children, cur_user=apiuser)
731 734
732 735 action_data = {
733 736 'added': changes['added'],
734 737 'updated': changes['updated'],
735 738 'deleted': changes['deleted'],
736 739 }
737 740 audit_logger.store_api(
738 741 'repo_group.edit.permissions', action_data=action_data,
739 742 user=apiuser)
740 743 Session().commit()
741 744 PermissionModel().flush_user_permission_caches(changes)
742 745
743 746 return {
744 747 'msg': 'Revoked perm (recursive:%s) for user group: '
745 748 '`%s` in repo group: `%s`' % (
746 749 apply_to_children, user_group.users_group_name,
747 750 repo_group.name
748 751 ),
749 752 'success': True
750 753 }
751 754 except Exception:
752 755 log.exception("Exception occurred while trying revoke user group "
753 756 "permissions from repo group")
754 757 raise JSONRPCError(
755 758 'failed to edit permission for user group: '
756 759 '`%s` in repo group: `%s`' % (
757 760 user_group.users_group_name, repo_group.name
758 761 )
759 762 )
@@ -1,362 +1,356 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import datetime
21 21 import logging
22 22 import time
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 28
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 34
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, CSRFRequired, NotAnonymous,
37 37 HasPermissionAny, HasRepoGroupPermissionAny)
38 38 from rhodecode.lib import helpers as h, audit_logger
39 39 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
40 40 from rhodecode.model.forms import RepoGroupForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList
44 44 from rhodecode.model.db import (
45 45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 allow_empty_group = False
59 59
60 60 if self._can_create_repo_group():
61 61 # we're global admin, we're ok and we can create TOP level groups
62 62 allow_empty_group = True
63 63
64 64 # override the choices for this form, we need to filter choices
65 65 # and display only those we have ADMIN right
66 66 groups_with_admin_rights = RepoGroupList(
67 67 RepoGroup.query().all(),
68 68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 69 c.repo_groups = RepoGroup.groups_choices(
70 70 groups=groups_with_admin_rights,
71 71 show_empty_group=allow_empty_group)
72 72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73 73
74 74 def _can_create_repo_group(self, parent_group_id=None):
75 75 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 76 create_repo_group = HasPermissionAny(
77 77 'hg.repogroup.create.true')('group create controller')
78 78 if is_admin or (create_repo_group and not parent_group_id):
79 79 # we're global admin, or we have global repo group create
80 80 # permission
81 81 # we're ok and we can create TOP level groups
82 82 return True
83 83 elif parent_group_id:
84 84 # we check the permission if we can write to parent group
85 85 group = RepoGroup.get(parent_group_id)
86 86 group_name = group.group_name if group else None
87 87 if HasRepoGroupPermissionAny('group.admin')(
88 88 group_name, 'check if user is an admin of group'):
89 89 # we're an admin of passed in group, we're ok.
90 90 return True
91 91 else:
92 92 return False
93 93 return False
94 94
95 95 # permission check in data loading of
96 96 # `repo_group_list_data` via RepoGroupList
97 97 @LoginRequired()
98 98 @NotAnonymous()
99 99 def repo_group_list(self):
100 100 c = self.load_default_context()
101 101 return self._get_template_context(c)
102 102
103 103 # permission check inside
104 104 @LoginRequired()
105 105 @NotAnonymous()
106 106 def repo_group_list_data(self):
107 107 self.load_default_context()
108 108 column_map = {
109 109 'name': 'group_name_hash',
110 110 'desc': 'group_description',
111 111 'last_change': 'updated_on',
112 112 'top_level_repos': 'repos_total',
113 113 'owner': 'user_username',
114 114 }
115 115 draw, start, limit = self._extract_chunk(self.request)
116 116 search_q, order_by, order_dir = self._extract_ordering(
117 117 self.request, column_map=column_map)
118 118
119 119 _render = self.request.get_partial_renderer(
120 120 'rhodecode:templates/data_table/_dt_elements.mako')
121 121 c = _render.get_call_context()
122 122
123 123 def quick_menu(repo_group_name):
124 124 return _render('quick_repo_group_menu', repo_group_name)
125 125
126 126 def repo_group_lnk(repo_group_name):
127 127 return _render('repo_group_name', repo_group_name)
128 128
129 129 def last_change(last_change):
130 130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
131 131 ts = time.time()
132 132 utc_offset = (datetime.datetime.fromtimestamp(ts)
133 133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
134 134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
135 135 return _render("last_change", last_change)
136 136
137 137 def desc(desc, personal):
138 138 return _render(
139 139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
140 140
141 141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
142 142 return _render(
143 143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
144 144
145 145 def user_profile(username):
146 146 return _render('user_profile', username)
147 147
148 148 _perms = ['group.admin']
149 149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
150 150
151 151 repo_groups_data_total_count = RepoGroup.query()\
152 152 .filter(or_(
153 153 # generate multiple IN to fix limitation problems
154 154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
155 155 )) \
156 156 .count()
157 157
158 158 repo_groups_data_total_inactive_count = RepoGroup.query()\
159 159 .filter(RepoGroup.group_id.in_(allowed_ids))\
160 160 .count()
161 161
162 162 repo_count = count(Repository.repo_id)
163 163 base_q = Session.query(
164 164 RepoGroup.group_name,
165 165 RepoGroup.group_name_hash,
166 166 RepoGroup.group_description,
167 167 RepoGroup.group_id,
168 168 RepoGroup.personal,
169 169 RepoGroup.updated_on,
170 170 User,
171 171 repo_count.label('repos_count')
172 172 ) \
173 173 .filter(or_(
174 174 # generate multiple IN to fix limitation problems
175 175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
176 176 )) \
177 177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
178 178 .join(User, User.user_id == RepoGroup.user_id) \
179 179 .group_by(RepoGroup, User)
180 180
181 181 if search_q:
182 182 like_expression = u'%{}%'.format(safe_unicode(search_q))
183 183 base_q = base_q.filter(or_(
184 184 RepoGroup.group_name.ilike(like_expression),
185 185 ))
186 186
187 187 repo_groups_data_total_filtered_count = base_q.count()
188 188 # the inactive isn't really used, but we still make it same as other data grids
189 189 # which use inactive (users,user groups)
190 190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191 191
192 192 sort_defined = False
193 193 if order_by == 'group_name':
194 194 sort_col = func.lower(RepoGroup.group_name)
195 195 sort_defined = True
196 196 elif order_by == 'repos_total':
197 197 sort_col = repo_count
198 198 sort_defined = True
199 199 elif order_by == 'user_username':
200 200 sort_col = User.username
201 201 else:
202 202 sort_col = getattr(RepoGroup, order_by, None)
203 203
204 204 if sort_defined or sort_col:
205 205 if order_dir == 'asc':
206 206 sort_col = sort_col.asc()
207 207 else:
208 208 sort_col = sort_col.desc()
209 209
210 210 base_q = base_q.order_by(sort_col)
211 211 base_q = base_q.offset(start).limit(limit)
212 212
213 213 # authenticated access to user groups
214 214 auth_repo_group_list = base_q.all()
215 215
216 216 repo_groups_data = []
217 217 for repo_gr in auth_repo_group_list:
218 218 row = {
219 219 "menu": quick_menu(repo_gr.group_name),
220 220 "name": repo_group_lnk(repo_gr.group_name),
221 221
222 222 "last_change": last_change(repo_gr.updated_on),
223 223
224 224 "last_changeset": "",
225 225 "last_changeset_raw": "",
226 226
227 227 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 228 "owner": user_profile(repo_gr.User.username),
229 229 "top_level_repos": repo_gr.repos_count,
230 230 "action": repo_group_actions(
231 231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232 232
233 233 }
234 234
235 235 repo_groups_data.append(row)
236 236
237 237 data = ({
238 238 'draw': draw,
239 239 'data': repo_groups_data,
240 240 'recordsTotal': repo_groups_data_total_count,
241 241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
242 242 'recordsFiltered': repo_groups_data_total_filtered_count,
243 243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
244 244 })
245 245
246 246 return data
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 # perm checks inside
251 251 def repo_group_new(self):
252 252 c = self.load_default_context()
253 253
254 254 # perm check for admin, create_group perm or admin of parent_group
255 255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
256 256 _gr = RepoGroup.get(parent_group_id)
257 257 if not self._can_create_repo_group(parent_group_id):
258 258 raise HTTPForbidden()
259 259
260 260 self._load_form_data(c)
261 261
262 262 defaults = {} # Future proof for default of repo group
263 263
264 264 parent_group_choice = '-1'
265 265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
266 266 parent_group_choice = self._rhodecode_user.personal_repo_group
267 267
268 268 if parent_group_id and _gr:
269 269 if parent_group_id in [x[0] for x in c.repo_groups]:
270 270 parent_group_choice = safe_unicode(parent_group_id)
271 271
272 272 defaults.update({'group_parent_id': parent_group_choice})
273 273
274 274 data = render(
275 275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
276 276 self._get_template_context(c), self.request)
277 277
278 278 html = formencode.htmlfill.render(
279 279 data,
280 280 defaults=defaults,
281 281 encoding="UTF-8",
282 282 force_defaults=False
283 283 )
284 284 return Response(html)
285 285
286 286 @LoginRequired()
287 287 @NotAnonymous()
288 288 @CSRFRequired()
289 289 # perm checks inside
290 290 def repo_group_create(self):
291 291 c = self.load_default_context()
292 292 _ = self.request.translate
293 293
294 294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 295 can_create = self._can_create_repo_group(parent_group_id)
296 296
297 297 self._load_form_data(c)
298 298 # permissions for can create group based on parent_id are checked
299 299 # here in the Form
300 300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
301 301 repo_group_form = RepoGroupForm(
302 302 self.request.translate, available_groups=available_groups,
303 303 can_create_in_root=can_create)()
304 304
305 305 repo_group_name = self.request.POST.get('group_name')
306 306 try:
307 307 owner = self._rhodecode_user
308 308 form_result = repo_group_form.to_python(dict(self.request.POST))
309 309 copy_permissions = form_result.get('group_copy_permissions')
310 310 repo_group = RepoGroupModel().create(
311 311 group_name=form_result['group_name_full'],
312 312 group_description=form_result['group_description'],
313 313 owner=owner.user_id,
314 314 copy_permissions=form_result['group_copy_permissions']
315 315 )
316 316 Session().flush()
317 317
318 318 repo_group_data = repo_group.get_api_data()
319 319 audit_logger.store_web(
320 320 'repo_group.create', action_data={'data': repo_group_data},
321 321 user=self._rhodecode_user)
322 322
323 323 Session().commit()
324 324
325 325 _new_group_name = form_result['group_name_full']
326 326
327 327 repo_group_url = h.link_to(
328 328 _new_group_name,
329 329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 330 h.flash(h.literal(_('Created repository group %s')
331 331 % repo_group_url), category='success')
332 332
333 333 except formencode.Invalid as errors:
334 334 data = render(
335 335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 336 self._get_template_context(c), self.request)
337 337 html = formencode.htmlfill.render(
338 338 data,
339 339 defaults=errors.value,
340 340 errors=errors.error_dict or {},
341 341 prefix_error=False,
342 342 encoding="UTF-8",
343 343 force_defaults=False
344 344 )
345 345 return Response(html)
346 346 except Exception:
347 347 log.exception("Exception during creation of repository group")
348 348 h.flash(_('Error occurred during creation of repository group %s')
349 349 % repo_group_name, category='error')
350 350 raise HTTPFound(h.route_path('home'))
351 351
352 affected_user_ids = [self._rhodecode_user.user_id]
353 if copy_permissions:
354 user_group_perms = repo_group.permissions(expand_from_user_groups=True)
355 copy_perms = [perm['user_id'] for perm in user_group_perms]
356 # also include those newly created by copy
357 affected_user_ids.extend(copy_perms)
358 PermissionModel().trigger_permission_flush(affected_user_ids)
352 PermissionModel().trigger_permission_flush()
359 353
360 354 raise HTTPFound(
361 355 h.route_path('repo_group_home',
362 356 repo_group_name=form_result['group_name_full']))
@@ -1,253 +1,249 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 logging
22 22 import formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.celerylib.utils import get_task_id
33 33
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, CSRFRequired, NotAnonymous,
36 36 HasPermissionAny, HasRepoGroupPermissionAny)
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.utils import repo_name_slug
39 39 from rhodecode.lib.utils2 import safe_int, safe_unicode
40 40 from rhodecode.model.forms import RepoForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.db import (
46 46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class AdminReposView(BaseAppView, DataGridAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 59 perm_set=['group.write', 'group.admin'])
60 60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
62 62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63 63
64 64 @LoginRequired()
65 65 @NotAnonymous()
66 66 # perms check inside
67 67 def repository_list(self):
68 68 c = self.load_default_context()
69 69 return self._get_template_context(c)
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 # perms check inside
74 74 def repository_list_data(self):
75 75 self.load_default_context()
76 76 column_map = {
77 77 'name': 'repo_name',
78 78 'desc': 'description',
79 79 'last_change': 'updated_on',
80 80 'owner': 'user_username',
81 81 }
82 82 draw, start, limit = self._extract_chunk(self.request)
83 83 search_q, order_by, order_dir = self._extract_ordering(
84 84 self.request, column_map=column_map)
85 85
86 86 _perms = ['repository.admin']
87 87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
88 88
89 89 repos_data_total_count = Repository.query() \
90 90 .filter(or_(
91 91 # generate multiple IN to fix limitation problems
92 92 *in_filter_generator(Repository.repo_id, allowed_ids))
93 93 ) \
94 94 .count()
95 95
96 96 base_q = Session.query(
97 97 Repository.repo_id,
98 98 Repository.repo_name,
99 99 Repository.description,
100 100 Repository.repo_type,
101 101 Repository.repo_state,
102 102 Repository.private,
103 103 Repository.archived,
104 104 Repository.fork,
105 105 Repository.updated_on,
106 106 Repository._changeset_cache,
107 107 User,
108 108 ) \
109 109 .filter(or_(
110 110 # generate multiple IN to fix limitation problems
111 111 *in_filter_generator(Repository.repo_id, allowed_ids))
112 112 ) \
113 113 .join(User, User.user_id == Repository.user_id) \
114 114 .group_by(Repository, User)
115 115
116 116 if search_q:
117 117 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 118 base_q = base_q.filter(or_(
119 119 Repository.repo_name.ilike(like_expression),
120 120 ))
121 121
122 122 repos_data_total_filtered_count = base_q.count()
123 123
124 124 sort_defined = False
125 125 if order_by == 'repo_name':
126 126 sort_col = func.lower(Repository.repo_name)
127 127 sort_defined = True
128 128 elif order_by == 'user_username':
129 129 sort_col = User.username
130 130 else:
131 131 sort_col = getattr(Repository, order_by, None)
132 132
133 133 if sort_defined or sort_col:
134 134 if order_dir == 'asc':
135 135 sort_col = sort_col.asc()
136 136 else:
137 137 sort_col = sort_col.desc()
138 138
139 139 base_q = base_q.order_by(sort_col)
140 140 base_q = base_q.offset(start).limit(limit)
141 141
142 142 repos_list = base_q.all()
143 143
144 144 repos_data = RepoModel().get_repos_as_dict(
145 145 repo_list=repos_list, admin=True, super_user_actions=True)
146 146
147 147 data = ({
148 148 'draw': draw,
149 149 'data': repos_data,
150 150 'recordsTotal': repos_data_total_count,
151 151 'recordsFiltered': repos_data_total_filtered_count,
152 152 })
153 153 return data
154 154
155 155 @LoginRequired()
156 156 @NotAnonymous()
157 157 # perms check inside
158 158 def repository_new(self):
159 159 c = self.load_default_context()
160 160
161 161 new_repo = self.request.GET.get('repo', '')
162 162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
163 163 _gr = RepoGroup.get(parent_group_id)
164 164
165 165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
166 166 # you're not super admin nor have global create permissions,
167 167 # but maybe you have at least write permission to a parent group ?
168 168
169 169 gr_name = _gr.group_name if _gr else None
170 170 # create repositories with write permission on group is set to true
171 171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
172 172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
173 173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
174 174 if not (group_admin or (group_write and create_on_write)):
175 175 raise HTTPForbidden()
176 176
177 177 self._load_form_data(c)
178 178 c.new_repo = repo_name_slug(new_repo)
179 179
180 180 # apply the defaults from defaults page
181 181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
182 182 # set checkbox to autochecked
183 183 defaults['repo_copy_permissions'] = True
184 184
185 185 parent_group_choice = '-1'
186 186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
187 187 parent_group_choice = self._rhodecode_user.personal_repo_group
188 188
189 189 if parent_group_id and _gr:
190 190 if parent_group_id in [x[0] for x in c.repo_groups]:
191 191 parent_group_choice = safe_unicode(parent_group_id)
192 192
193 193 defaults.update({'repo_group': parent_group_choice})
194 194
195 195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
196 196 self._get_template_context(c), self.request)
197 197 html = formencode.htmlfill.render(
198 198 data,
199 199 defaults=defaults,
200 200 encoding="UTF-8",
201 201 force_defaults=False
202 202 )
203 203 return Response(html)
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 207 @CSRFRequired()
208 208 # perms check inside
209 209 def repository_create(self):
210 210 c = self.load_default_context()
211 211
212 212 form_result = {}
213 213 self._load_form_data(c)
214 214
215 215 try:
216 216 # CanWriteToGroup validators checks permissions of this POST
217 217 form = RepoForm(
218 218 self.request.translate, repo_groups=c.repo_groups_choices)()
219 219 form_result = form.to_python(dict(self.request.POST))
220 220 copy_permissions = form_result.get('repo_copy_permissions')
221 221 # create is done sometimes async on celery, db transaction
222 222 # management is handled there.
223 223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
224 224 task_id = get_task_id(task)
225 225 except formencode.Invalid as errors:
226 226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
227 227 self._get_template_context(c), self.request)
228 228 html = formencode.htmlfill.render(
229 229 data,
230 230 defaults=errors.value,
231 231 errors=errors.error_dict or {},
232 232 prefix_error=False,
233 233 encoding="UTF-8",
234 234 force_defaults=False
235 235 )
236 236 return Response(html)
237 237
238 238 except Exception as e:
239 239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
240 240 h.flash(msg, category='error')
241 241 raise HTTPFound(h.route_path('home'))
242 242
243 243 repo_name = form_result.get('repo_name_full')
244 244
245 affected_user_ids = [self._rhodecode_user.user_id]
246 if copy_permissions:
247 # permission flush is done in repo creating
248 pass
249 PermissionModel().trigger_permission_flush(affected_user_ids)
245 PermissionModel().trigger_permission_flush()
250 246
251 247 raise HTTPFound(
252 248 h.route_path('repo_creating', repo_name=repo_name,
253 249 _query=dict(task_id=task_id)))
@@ -1,450 +1,450 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 import colander
22 22 import string
23 23 import collections
24 24 import logging
25 25 import requests
26 26 import urllib
27 27 from requests.adapters import HTTPAdapter
28 28 from requests.packages.urllib3.util.retry import Retry
29 29
30 30 from mako import exceptions
31 31
32 32 from rhodecode.lib.utils2 import safe_str
33 33 from rhodecode.translation import _
34 34
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class UrlTmpl(string.Template):
40 40
41 41 def safe_substitute(self, **kws):
42 42 # url encode the kw for usage in url
43 43 kws = {k: urllib.quote(safe_str(v)) for k, v in kws.items()}
44 44 return super(UrlTmpl, self).safe_substitute(**kws)
45 45
46 46
47 47 class IntegrationTypeBase(object):
48 48 """ Base class for IntegrationType plugins """
49 49 is_dummy = False
50 50 description = ''
51 51
52 52 @classmethod
53 53 def icon(cls):
54 54 return '''
55 55 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
56 56 <svg
57 57 xmlns:dc="http://purl.org/dc/elements/1.1/"
58 58 xmlns:cc="http://creativecommons.org/ns#"
59 59 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
60 60 xmlns:svg="http://www.w3.org/2000/svg"
61 61 xmlns="http://www.w3.org/2000/svg"
62 62 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
63 63 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
64 64 viewBox="0 -256 1792 1792"
65 65 id="svg3025"
66 66 version="1.1"
67 67 inkscape:version="0.48.3.1 r9886"
68 68 width="100%"
69 69 height="100%"
70 70 sodipodi:docname="cog_font_awesome.svg">
71 71 <metadata
72 72 id="metadata3035">
73 73 <rdf:RDF>
74 74 <cc:Work
75 75 rdf:about="">
76 76 <dc:format>image/svg+xml</dc:format>
77 77 <dc:type
78 78 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
79 79 </cc:Work>
80 80 </rdf:RDF>
81 81 </metadata>
82 82 <defs
83 83 id="defs3033" />
84 84 <sodipodi:namedview
85 85 pagecolor="#ffffff"
86 86 bordercolor="#666666"
87 87 borderopacity="1"
88 88 objecttolerance="10"
89 89 gridtolerance="10"
90 90 guidetolerance="10"
91 91 inkscape:pageopacity="0"
92 92 inkscape:pageshadow="2"
93 93 inkscape:window-width="640"
94 94 inkscape:window-height="480"
95 95 id="namedview3031"
96 96 showgrid="false"
97 97 inkscape:zoom="0.13169643"
98 98 inkscape:cx="896"
99 99 inkscape:cy="896"
100 100 inkscape:window-x="0"
101 101 inkscape:window-y="25"
102 102 inkscape:window-maximized="0"
103 103 inkscape:current-layer="svg3025" />
104 104 <g
105 105 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
106 106 id="g3027">
107 107 <path
108 108 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
109 109 id="path3029"
110 110 inkscape:connector-curvature="0"
111 111 style="fill:currentColor" />
112 112 </g>
113 113 </svg>
114 114 '''
115 115
116 116 def __init__(self, settings):
117 117 """
118 118 :param settings: dict of settings to be used for the integration
119 119 """
120 120 self.settings = settings
121 121
122 122 def settings_schema(self):
123 123 """
124 124 A colander schema of settings for the integration type
125 125 """
126 126 return colander.Schema()
127 127
128 128 def event_enabled(self, event):
129 129 """
130 130 Checks if submitted event is enabled based on the plugin settings
131 131 :param event:
132 132 :return: bool
133 133 """
134 allowed_events = self.settings['events']
134 allowed_events = self.settings.get('events') or []
135 135 if event.name not in allowed_events:
136 136 log.debug('event ignored: %r event %s not in allowed set of events %s',
137 137 event, event.name, allowed_events)
138 138 return False
139 139 return True
140 140
141 141
142 142 class EEIntegration(IntegrationTypeBase):
143 143 description = 'Integration available in RhodeCode EE edition.'
144 144 is_dummy = True
145 145
146 146 def __init__(self, name, key, settings=None):
147 147 self.display_name = name
148 148 self.key = key
149 149 super(EEIntegration, self).__init__(settings)
150 150
151 151
152 152 # Helpers #
153 153 # updating this required to update the `common_vars` as well.
154 154 WEBHOOK_URL_VARS = [
155 155 # GENERAL
156 156 ('General', [
157 157 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
158 158 ('repo_name', 'Full name of the repository'),
159 159 ('repo_type', 'VCS type of repository'),
160 160 ('repo_id', 'Unique id of repository'),
161 161 ('repo_url', 'Repository url'),
162 162 ]
163 163 ),
164 164 # extra repo fields
165 165 ('Repository', [
166 166 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
167 167 ]
168 168 ),
169 169 # special attrs below that we handle, using multi-call
170 170 ('Commit push - Multicalls', [
171 171 ('branch', 'Name of each branch submitted, if any.'),
172 172 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
173 173 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
174 174 ]
175 175 ),
176 176 # pr events vars
177 177 ('Pull request', [
178 178 ('pull_request_id', 'Unique ID of the pull request.'),
179 179 ('pull_request_title', 'Title of the pull request.'),
180 180 ('pull_request_url', 'Pull request url.'),
181 181 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
182 182 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
183 183 'Changes after PR update'),
184 184 ]
185 185 ),
186 186 # commit comment event vars
187 187 ('Commit comment', [
188 188 ('commit_comment_id', 'Unique ID of the comment made on a commit.'),
189 189 ('commit_comment_text', 'Text of commit comment.'),
190 190 ('commit_comment_type', 'Type of comment, e.g note/todo.'),
191 191
192 192 ('commit_comment_f_path', 'Optionally path of file for inline comments.'),
193 193 ('commit_comment_line_no', 'Line number of the file: eg o10, or n200'),
194 194
195 195 ('commit_comment_commit_id', 'Commit id that comment was left at.'),
196 196 ('commit_comment_commit_branch', 'Commit branch that comment was left at'),
197 197 ('commit_comment_commit_message', 'Commit message that comment was left at'),
198 198 ]
199 199 ),
200 200 # user who triggers the call
201 201 ('Caller', [
202 202 ('username', 'User who triggered the call.'),
203 203 ('user_id', 'User id who triggered the call.'),
204 204 ]
205 205 ),
206 206 ]
207 207
208 208 # common vars for url template used for CI plugins. Shared with webhook
209 209 CI_URL_VARS = WEBHOOK_URL_VARS
210 210
211 211
212 212 class CommitParsingDataHandler(object):
213 213
214 214 def aggregate_branch_data(self, branches, commits):
215 215 branch_data = collections.OrderedDict()
216 216 for obj in branches:
217 217 branch_data[obj['name']] = obj
218 218
219 219 branches_commits = collections.OrderedDict()
220 220 for commit in commits:
221 221 if commit.get('git_ref_change'):
222 222 # special case for GIT that allows creating tags,
223 223 # deleting branches without associated commit
224 224 continue
225 225 commit_branch = commit['branch']
226 226
227 227 if commit_branch not in branches_commits:
228 228 _branch = branch_data[commit_branch] \
229 229 if commit_branch else commit_branch
230 230 branch_commits = {'branch': _branch,
231 231 'branch_head': '',
232 232 'commits': []}
233 233 branches_commits[commit_branch] = branch_commits
234 234
235 235 branch_commits = branches_commits[commit_branch]
236 236 branch_commits['commits'].append(commit)
237 237 branch_commits['branch_head'] = commit['raw_id']
238 238 return branches_commits
239 239
240 240
241 241 class WebhookDataHandler(CommitParsingDataHandler):
242 242 name = 'webhook'
243 243
244 244 def __init__(self, template_url, headers):
245 245 self.template_url = template_url
246 246 self.headers = headers
247 247
248 248 def get_base_parsed_template(self, data):
249 249 """
250 250 initially parses the passed in template with some common variables
251 251 available on ALL calls
252 252 """
253 253 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
254 254 common_vars = {
255 255 'repo_name': data['repo']['repo_name'],
256 256 'repo_type': data['repo']['repo_type'],
257 257 'repo_id': data['repo']['repo_id'],
258 258 'repo_url': data['repo']['url'],
259 259 'username': data['actor']['username'],
260 260 'user_id': data['actor']['user_id'],
261 261 'event_name': data['name']
262 262 }
263 263
264 264 extra_vars = {}
265 265 for extra_key, extra_val in data['repo']['extra_fields'].items():
266 266 extra_vars['extra__{}'.format(extra_key)] = extra_val
267 267 common_vars.update(extra_vars)
268 268
269 269 template_url = self.template_url.replace('${extra:', '${extra__')
270 270 for k, v in common_vars.items():
271 271 template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
272 272 return template_url
273 273
274 274 def repo_push_event_handler(self, event, data):
275 275 url = self.get_base_parsed_template(data)
276 276 url_calls = []
277 277
278 278 branches_commits = self.aggregate_branch_data(
279 279 data['push']['branches'], data['push']['commits'])
280 280 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
281 281 # call it multiple times, for each branch if used in variables
282 282 for branch, commit_ids in branches_commits.items():
283 283 branch_url = UrlTmpl(url).safe_substitute(branch=branch)
284 284
285 285 if '${branch_head}' in branch_url:
286 286 # last commit in the aggregate is the head of the branch
287 287 branch_head = commit_ids['branch_head']
288 288 branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
289 289
290 290 # call further down for each commit if used
291 291 if '${commit_id}' in branch_url:
292 292 for commit_data in commit_ids['commits']:
293 293 commit_id = commit_data['raw_id']
294 294 commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
295 295 # register per-commit call
296 296 log.debug(
297 297 'register %s call(%s) to url %s',
298 298 self.name, event, commit_url)
299 299 url_calls.append(
300 300 (commit_url, self.headers, data))
301 301
302 302 else:
303 303 # register per-branch call
304 304 log.debug('register %s call(%s) to url %s',
305 305 self.name, event, branch_url)
306 306 url_calls.append((branch_url, self.headers, data))
307 307
308 308 else:
309 309 log.debug('register %s call(%s) to url %s', self.name, event, url)
310 310 url_calls.append((url, self.headers, data))
311 311
312 312 return url_calls
313 313
314 314 def repo_commit_comment_handler(self, event, data):
315 315 url = self.get_base_parsed_template(data)
316 316 log.debug('register %s call(%s) to url %s', self.name, event, url)
317 317 comment_vars = [
318 318 ('commit_comment_id', data['comment']['comment_id']),
319 319 ('commit_comment_text', data['comment']['comment_text']),
320 320 ('commit_comment_type', data['comment']['comment_type']),
321 321
322 322 ('commit_comment_f_path', data['comment']['comment_f_path']),
323 323 ('commit_comment_line_no', data['comment']['comment_line_no']),
324 324
325 325 ('commit_comment_commit_id', data['commit']['commit_id']),
326 326 ('commit_comment_commit_branch', data['commit']['commit_branch']),
327 327 ('commit_comment_commit_message', data['commit']['commit_message']),
328 328 ]
329 329 for k, v in comment_vars:
330 330 url = UrlTmpl(url).safe_substitute(**{k: v})
331 331
332 332 return [(url, self.headers, data)]
333 333
334 334 def repo_commit_comment_edit_handler(self, event, data):
335 335 url = self.get_base_parsed_template(data)
336 336 log.debug('register %s call(%s) to url %s', self.name, event, url)
337 337 comment_vars = [
338 338 ('commit_comment_id', data['comment']['comment_id']),
339 339 ('commit_comment_text', data['comment']['comment_text']),
340 340 ('commit_comment_type', data['comment']['comment_type']),
341 341
342 342 ('commit_comment_f_path', data['comment']['comment_f_path']),
343 343 ('commit_comment_line_no', data['comment']['comment_line_no']),
344 344
345 345 ('commit_comment_commit_id', data['commit']['commit_id']),
346 346 ('commit_comment_commit_branch', data['commit']['commit_branch']),
347 347 ('commit_comment_commit_message', data['commit']['commit_message']),
348 348 ]
349 349 for k, v in comment_vars:
350 350 url = UrlTmpl(url).safe_substitute(**{k: v})
351 351
352 352 return [(url, self.headers, data)]
353 353
354 354 def repo_create_event_handler(self, event, data):
355 355 url = self.get_base_parsed_template(data)
356 356 log.debug('register %s call(%s) to url %s', self.name, event, url)
357 357 return [(url, self.headers, data)]
358 358
359 359 def pull_request_event_handler(self, event, data):
360 360 url = self.get_base_parsed_template(data)
361 361 log.debug('register %s call(%s) to url %s', self.name, event, url)
362 362 pr_vars = [
363 363 ('pull_request_id', data['pullrequest']['pull_request_id']),
364 364 ('pull_request_title', data['pullrequest']['title']),
365 365 ('pull_request_url', data['pullrequest']['url']),
366 366 ('pull_request_shadow_url', data['pullrequest']['shadow_url']),
367 367 ('pull_request_commits_uid', data['pullrequest']['commits_uid']),
368 368 ]
369 369 for k, v in pr_vars:
370 370 url = UrlTmpl(url).safe_substitute(**{k: v})
371 371
372 372 return [(url, self.headers, data)]
373 373
374 374 def __call__(self, event, data):
375 375 from rhodecode import events
376 376
377 377 if isinstance(event, events.RepoPushEvent):
378 378 return self.repo_push_event_handler(event, data)
379 379 elif isinstance(event, events.RepoCreateEvent):
380 380 return self.repo_create_event_handler(event, data)
381 381 elif isinstance(event, events.RepoCommitCommentEvent):
382 382 return self.repo_commit_comment_handler(event, data)
383 383 elif isinstance(event, events.RepoCommitCommentEditEvent):
384 384 return self.repo_commit_comment_edit_handler(event, data)
385 385 elif isinstance(event, events.PullRequestEvent):
386 386 return self.pull_request_event_handler(event, data)
387 387 else:
388 388 raise ValueError(
389 389 'event type `{}` has no handler defined'.format(event.__class__))
390 390
391 391
392 392 def get_auth(settings):
393 393 from requests.auth import HTTPBasicAuth
394 394 username = settings.get('username')
395 395 password = settings.get('password')
396 396 if username and password:
397 397 return HTTPBasicAuth(username, password)
398 398 return None
399 399
400 400
401 401 def get_web_token(settings):
402 402 return settings['secret_token']
403 403
404 404
405 405 def get_url_vars(url_vars):
406 406 items = []
407 407
408 408 for section, section_items in url_vars:
409 409 items.append('\n*{}*'.format(section))
410 410 for key, explanation in section_items:
411 411 items.append(' {} - {}'.format('${' + key + '}', explanation))
412 412 return '\n'.join(items)
413 413
414 414
415 415 def render_with_traceback(template, *args, **kwargs):
416 416 try:
417 417 return template.render(*args, **kwargs)
418 418 except Exception:
419 419 log.error(exceptions.text_error_template().render())
420 420 raise
421 421
422 422
423 423 STATUS_400 = (400, 401, 403)
424 424 STATUS_500 = (500, 502, 504)
425 425
426 426
427 427 def requests_retry_call(
428 428 retries=3, backoff_factor=0.3, status_forcelist=STATUS_400+STATUS_500,
429 429 session=None):
430 430 """
431 431 session = requests_retry_session()
432 432 response = session.get('http://example.com')
433 433
434 434 :param retries:
435 435 :param backoff_factor:
436 436 :param status_forcelist:
437 437 :param session:
438 438 """
439 439 session = session or requests.Session()
440 440 retry = Retry(
441 441 total=retries,
442 442 read=retries,
443 443 connect=retries,
444 444 backoff_factor=backoff_factor,
445 445 status_forcelist=status_forcelist,
446 446 )
447 447 adapter = HTTPAdapter(max_retries=retry)
448 448 session.mount('http://', adapter)
449 449 session.mount('https://', adapter)
450 450 return session
@@ -1,403 +1,407 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 from email.utils import formatdate
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import audit_logger
36 36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task
37 37 from rhodecode.lib import hooks_base
38 38 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
39 39 from rhodecode.model.db import (
40 40 Session, IntegrityError, true, Repository, RepoGroup, User)
41 from rhodecode.model.permission import PermissionModel
41 42
42 43
43 44 @async_task(ignore_result=True, base=RequestContextTask)
44 45 def send_email(recipients, subject, body='', html_body='', email_config=None,
45 46 extra_headers=None):
46 47 """
47 48 Sends an email with defined parameters from the .ini files.
48 49
49 50 :param recipients: list of recipients, it this is empty the defined email
50 51 address from field 'email_to' is used instead
51 52 :param subject: subject of the mail
52 53 :param body: body of the mail
53 54 :param html_body: html version of body
54 55 :param email_config: specify custom configuration for mailer
55 56 :param extra_headers: specify custom headers
56 57 """
57 58 log = get_logger(send_email)
58 59
59 60 email_config = email_config or rhodecode.CONFIG
60 61
61 62 mail_server = email_config.get('smtp_server') or None
62 63 if mail_server is None:
63 64 log.error("SMTP server information missing. Sending email failed. "
64 65 "Make sure that `smtp_server` variable is configured "
65 66 "inside the .ini file")
66 67 return False
67 68
68 69 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
69 70
70 71 if recipients:
71 72 if isinstance(recipients, compat.string_types):
72 73 recipients = recipients.split(',')
73 74 else:
74 75 # if recipients are not defined we send to email_config + all admins
75 76 admins = []
76 77 for u in User.query().filter(User.admin == true()).all():
77 78 if u.email:
78 79 admins.append(u.email)
79 80 recipients = []
80 81 config_email = email_config.get('email_to')
81 82 if config_email:
82 83 recipients += [config_email]
83 84 recipients += admins
84 85
85 86 # translate our LEGACY config into the one that pyramid_mailer supports
86 87 email_conf = dict(
87 88 host=mail_server,
88 89 port=email_config.get('smtp_port', 25),
89 90 username=email_config.get('smtp_username'),
90 91 password=email_config.get('smtp_password'),
91 92
92 93 tls=str2bool(email_config.get('smtp_use_tls')),
93 94 ssl=str2bool(email_config.get('smtp_use_ssl')),
94 95
95 96 # SSL key file
96 97 # keyfile='',
97 98
98 99 # SSL certificate file
99 100 # certfile='',
100 101
101 102 # Location of maildir
102 103 # queue_path='',
103 104
104 105 default_sender=email_config.get('app_email_from', 'RhodeCode'),
105 106
106 107 debug=str2bool(email_config.get('smtp_debug')),
107 108 # /usr/sbin/sendmail Sendmail executable
108 109 # sendmail_app='',
109 110
110 111 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
111 112 # sendmail_template='',
112 113 )
113 114
114 115 if extra_headers is None:
115 116 extra_headers = {}
116 117
117 118 extra_headers.setdefault('Date', formatdate(time.time()))
118 119
119 120 if 'thread_ids' in extra_headers:
120 121 thread_ids = extra_headers.pop('thread_ids')
121 122 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122 123
123 124 try:
124 125 mailer = Mailer(**email_conf)
125 126
126 127 message = Message(subject=subject,
127 128 sender=email_conf['default_sender'],
128 129 recipients=recipients,
129 130 body=body, html=html_body,
130 131 extra_headers=extra_headers)
131 132 mailer.send_immediately(message)
132 133
133 134 except Exception:
134 135 log.exception('Mail sending failed')
135 136 return False
136 137 return True
137 138
138 139
139 140 @async_task(ignore_result=True, base=RequestContextTask)
140 141 def create_repo(form_data, cur_user):
141 142 from rhodecode.model.repo import RepoModel
142 143 from rhodecode.model.user import UserModel
143 144 from rhodecode.model.scm import ScmModel
144 145 from rhodecode.model.settings import SettingsModel
145 146
146 147 log = get_logger(create_repo)
147 148
148 149 cur_user = UserModel()._get_user(cur_user)
149 150 owner = cur_user
150 151
151 152 repo_name = form_data['repo_name']
152 153 repo_name_full = form_data['repo_name_full']
153 154 repo_type = form_data['repo_type']
154 155 description = form_data['repo_description']
155 156 private = form_data['repo_private']
156 157 clone_uri = form_data.get('clone_uri')
157 158 repo_group = safe_int(form_data['repo_group'])
158 159 copy_fork_permissions = form_data.get('copy_permissions')
159 160 copy_group_permissions = form_data.get('repo_copy_permissions')
160 161 fork_of = form_data.get('fork_parent_id')
161 162 state = form_data.get('repo_state', Repository.STATE_PENDING)
162 163
163 164 # repo creation defaults, private and repo_type are filled in form
164 165 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
165 166 enable_statistics = form_data.get(
166 167 'enable_statistics', defs.get('repo_enable_statistics'))
167 168 enable_locking = form_data.get(
168 169 'enable_locking', defs.get('repo_enable_locking'))
169 170 enable_downloads = form_data.get(
170 171 'enable_downloads', defs.get('repo_enable_downloads'))
171 172
172 173 # set landing rev based on default branches for SCM
173 174 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
174 175
175 176 try:
176 177 RepoModel()._create_repo(
177 178 repo_name=repo_name_full,
178 179 repo_type=repo_type,
179 180 description=description,
180 181 owner=owner,
181 182 private=private,
182 183 clone_uri=clone_uri,
183 184 repo_group=repo_group,
184 185 landing_rev=landing_ref,
185 186 fork_of=fork_of,
186 187 copy_fork_permissions=copy_fork_permissions,
187 188 copy_group_permissions=copy_group_permissions,
188 189 enable_statistics=enable_statistics,
189 190 enable_locking=enable_locking,
190 191 enable_downloads=enable_downloads,
191 192 state=state
192 193 )
193 194 Session().commit()
194 195
195 196 # now create this repo on Filesystem
196 197 RepoModel()._create_filesystem_repo(
197 198 repo_name=repo_name,
198 199 repo_type=repo_type,
199 200 repo_group=RepoModel()._get_repo_group(repo_group),
200 201 clone_uri=clone_uri,
201 202 )
202 203 repo = Repository.get_by_repo_name(repo_name_full)
203 204 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
204 205
205 206 # update repo commit caches initially
206 207 repo.update_commit_cache()
207 208
208 209 # set new created state
209 210 repo.set_state(Repository.STATE_CREATED)
210 211 repo_id = repo.repo_id
211 212 repo_data = repo.get_api_data()
212 213
213 214 audit_logger.store(
214 215 'repo.create', action_data={'data': repo_data},
215 216 user=cur_user,
216 217 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
217 218
218 219 Session().commit()
220
221 PermissionModel().trigger_permission_flush()
222
219 223 except Exception as e:
220 224 log.warning('Exception occurred when creating repository, '
221 225 'doing cleanup...', exc_info=True)
222 226 if isinstance(e, IntegrityError):
223 227 Session().rollback()
224 228
225 229 # rollback things manually !
226 230 repo = Repository.get_by_repo_name(repo_name_full)
227 231 if repo:
228 232 Repository.delete(repo.repo_id)
229 233 Session().commit()
230 234 RepoModel()._delete_filesystem_repo(repo)
231 235 log.info('Cleanup of repo %s finished', repo_name_full)
232 236 raise
233 237
234 238 return True
235 239
236 240
237 241 @async_task(ignore_result=True, base=RequestContextTask)
238 242 def create_repo_fork(form_data, cur_user):
239 243 """
240 244 Creates a fork of repository using internal VCS methods
241 245 """
242 246 from rhodecode.model.repo import RepoModel
243 247 from rhodecode.model.user import UserModel
244 248
245 249 log = get_logger(create_repo_fork)
246 250
247 251 cur_user = UserModel()._get_user(cur_user)
248 252 owner = cur_user
249 253
250 254 repo_name = form_data['repo_name'] # fork in this case
251 255 repo_name_full = form_data['repo_name_full']
252 256 repo_type = form_data['repo_type']
253 257 description = form_data['description']
254 258 private = form_data['private']
255 259 clone_uri = form_data.get('clone_uri')
256 260 repo_group = safe_int(form_data['repo_group'])
257 261 landing_ref = form_data['landing_rev']
258 262 copy_fork_permissions = form_data.get('copy_permissions')
259 263 fork_id = safe_int(form_data.get('fork_parent_id'))
260 264
261 265 try:
262 266 fork_of = RepoModel()._get_repo(fork_id)
263 267 RepoModel()._create_repo(
264 268 repo_name=repo_name_full,
265 269 repo_type=repo_type,
266 270 description=description,
267 271 owner=owner,
268 272 private=private,
269 273 clone_uri=clone_uri,
270 274 repo_group=repo_group,
271 275 landing_rev=landing_ref,
272 276 fork_of=fork_of,
273 277 copy_fork_permissions=copy_fork_permissions
274 278 )
275 279
276 280 Session().commit()
277 281
278 282 base_path = Repository.base_path()
279 283 source_repo_path = os.path.join(base_path, fork_of.repo_name)
280 284
281 285 # now create this repo on Filesystem
282 286 RepoModel()._create_filesystem_repo(
283 287 repo_name=repo_name,
284 288 repo_type=repo_type,
285 289 repo_group=RepoModel()._get_repo_group(repo_group),
286 290 clone_uri=source_repo_path,
287 291 )
288 292 repo = Repository.get_by_repo_name(repo_name_full)
289 293 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
290 294
291 295 # update repo commit caches initially
292 296 config = repo._config
293 297 config.set('extensions', 'largefiles', '')
294 298 repo.update_commit_cache(config=config)
295 299
296 300 # set new created state
297 301 repo.set_state(Repository.STATE_CREATED)
298 302
299 303 repo_id = repo.repo_id
300 304 repo_data = repo.get_api_data()
301 305 audit_logger.store(
302 306 'repo.fork', action_data={'data': repo_data},
303 307 user=cur_user,
304 308 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
305 309
306 310 Session().commit()
307 311 except Exception as e:
308 312 log.warning('Exception occurred when forking repository, '
309 313 'doing cleanup...', exc_info=True)
310 314 if isinstance(e, IntegrityError):
311 315 Session().rollback()
312 316
313 317 # rollback things manually !
314 318 repo = Repository.get_by_repo_name(repo_name_full)
315 319 if repo:
316 320 Repository.delete(repo.repo_id)
317 321 Session().commit()
318 322 RepoModel()._delete_filesystem_repo(repo)
319 323 log.info('Cleanup of repo %s finished', repo_name_full)
320 324 raise
321 325
322 326 return True
323 327
324 328
325 329 @async_task(ignore_result=True)
326 330 def repo_maintenance(repoid):
327 331 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
328 332 log = get_logger(repo_maintenance)
329 333 repo = Repository.get_by_id_or_repo_name(repoid)
330 334 if repo:
331 335 maintenance = repo_maintenance_lib.RepoMaintenance()
332 336 tasks = maintenance.get_tasks_for_repo(repo)
333 337 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
334 338 executed_types = maintenance.execute(repo)
335 339 log.debug('Got execution results %s', executed_types)
336 340 else:
337 341 log.debug('Repo `%s` not found or without a clone_url', repoid)
338 342
339 343
340 344 @async_task(ignore_result=True)
341 345 def check_for_update(send_email_notification=True, email_recipients=None):
342 346 from rhodecode.model.update import UpdateModel
343 347 from rhodecode.model.notification import EmailNotificationModel
344 348
345 349 log = get_logger(check_for_update)
346 350 update_url = UpdateModel().get_update_url()
347 351 cur_ver = rhodecode.__version__
348 352
349 353 try:
350 354 data = UpdateModel().get_update_data(update_url)
351 355
352 356 current_ver = UpdateModel().get_stored_version(fallback=cur_ver)
353 357 latest_ver = data['versions'][0]['version']
354 358 UpdateModel().store_version(latest_ver)
355 359
356 360 if send_email_notification:
357 361 log.debug('Send email notification is enabled. '
358 362 'Current RhodeCode version: %s, latest known: %s', current_ver, latest_ver)
359 363 if UpdateModel().is_outdated(current_ver, latest_ver):
360 364
361 365 email_kwargs = {
362 366 'current_ver': current_ver,
363 367 'latest_ver': latest_ver,
364 368 }
365 369
366 370 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
367 371 EmailNotificationModel.TYPE_UPDATE_AVAILABLE, **email_kwargs)
368 372
369 373 email_recipients = aslist(email_recipients, sep=',') or \
370 374 [user.email for user in User.get_all_super_admins()]
371 375 run_task(send_email, email_recipients, subject,
372 376 email_body_plaintext, email_body)
373 377
374 378 except Exception:
375 379 pass
376 380
377 381
378 382 @async_task(ignore_result=False)
379 383 def beat_check(*args, **kwargs):
380 384 log = get_logger(beat_check)
381 385 log.info('%r: Got args: %r and kwargs %r', beat_check, args, kwargs)
382 386 return time.time()
383 387
384 388
385 389 @async_task(ignore_result=True)
386 390 def sync_last_update(*args, **kwargs):
387 391
388 392 skip_repos = kwargs.get('skip_repos')
389 393 if not skip_repos:
390 394 repos = Repository.query() \
391 395 .order_by(Repository.group_id.asc())
392 396
393 397 for repo in repos:
394 398 repo.update_commit_cache()
395 399
396 400 skip_groups = kwargs.get('skip_groups')
397 401 if not skip_groups:
398 402 repo_groups = RepoGroup.query() \
399 403 .filter(RepoGroup.group_parent_id == None)
400 404
401 405 for root_gr in repo_groups:
402 406 for repo_gr in reversed(root_gr.recursive_groups()):
403 407 repo_gr.update_commit_cache()
General Comments 0
You need to be logged in to leave comments. Login now