##// END OF EJS Templates
audit-logs: implemented full audit logs across application....
marcink -
r1829:ff4add41 default
parent child Browse files
Show More
@@ -1,2062 +1,2070 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.utils2 import str2bool, time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 38 from rhodecode.model.changeset_status import ChangesetStatusModel
39 39 from rhodecode.model.comment import CommentsModel
40 40 from rhodecode.model.db import (
41 41 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
42 42 ChangesetComment)
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.scm import ScmModel, RepoList
45 45 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 46 from rhodecode.model import validation_schema
47 47 from rhodecode.model.validation_schema.schemas import repo_schema
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 @jsonrpc_method()
53 53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 54 """
55 55 Gets an existing repository by its name or repository_id.
56 56
57 57 The members section so the output returns users groups or users
58 58 associated with that repository.
59 59
60 60 This command can only be run using an |authtoken| with admin rights,
61 61 or users with at least read rights to the |repo|.
62 62
63 63 :param apiuser: This is filled automatically from the |authtoken|.
64 64 :type apiuser: AuthUser
65 65 :param repoid: The repository name or repository id.
66 66 :type repoid: str or int
67 67 :param cache: use the cached value for last changeset
68 68 :type: cache: Optional(bool)
69 69
70 70 Example output:
71 71
72 72 .. code-block:: bash
73 73
74 74 {
75 75 "error": null,
76 76 "id": <repo_id>,
77 77 "result": {
78 78 "clone_uri": null,
79 79 "created_on": "timestamp",
80 80 "description": "repo description",
81 81 "enable_downloads": false,
82 82 "enable_locking": false,
83 83 "enable_statistics": false,
84 84 "followers": [
85 85 {
86 86 "active": true,
87 87 "admin": false,
88 88 "api_key": "****************************************",
89 89 "api_keys": [
90 90 "****************************************"
91 91 ],
92 92 "email": "user@example.com",
93 93 "emails": [
94 94 "user@example.com"
95 95 ],
96 96 "extern_name": "rhodecode",
97 97 "extern_type": "rhodecode",
98 98 "firstname": "username",
99 99 "ip_addresses": [],
100 100 "language": null,
101 101 "last_login": "2015-09-16T17:16:35.854",
102 102 "lastname": "surname",
103 103 "user_id": <user_id>,
104 104 "username": "name"
105 105 }
106 106 ],
107 107 "fork_of": "parent-repo",
108 108 "landing_rev": [
109 109 "rev",
110 110 "tip"
111 111 ],
112 112 "last_changeset": {
113 113 "author": "User <user@example.com>",
114 114 "branch": "default",
115 115 "date": "timestamp",
116 116 "message": "last commit message",
117 117 "parents": [
118 118 {
119 119 "raw_id": "commit-id"
120 120 }
121 121 ],
122 122 "raw_id": "commit-id",
123 123 "revision": <revision number>,
124 124 "short_id": "short id"
125 125 },
126 126 "lock_reason": null,
127 127 "locked_by": null,
128 128 "locked_date": null,
129 129 "members": [
130 130 {
131 131 "name": "super-admin-name",
132 132 "origin": "super-admin",
133 133 "permission": "repository.admin",
134 134 "type": "user"
135 135 },
136 136 {
137 137 "name": "owner-name",
138 138 "origin": "owner",
139 139 "permission": "repository.admin",
140 140 "type": "user"
141 141 },
142 142 {
143 143 "name": "user-group-name",
144 144 "origin": "permission",
145 145 "permission": "repository.write",
146 146 "type": "user_group"
147 147 }
148 148 ],
149 149 "owner": "owner-name",
150 150 "permissions": [
151 151 {
152 152 "name": "super-admin-name",
153 153 "origin": "super-admin",
154 154 "permission": "repository.admin",
155 155 "type": "user"
156 156 },
157 157 {
158 158 "name": "owner-name",
159 159 "origin": "owner",
160 160 "permission": "repository.admin",
161 161 "type": "user"
162 162 },
163 163 {
164 164 "name": "user-group-name",
165 165 "origin": "permission",
166 166 "permission": "repository.write",
167 167 "type": "user_group"
168 168 }
169 169 ],
170 170 "private": true,
171 171 "repo_id": 676,
172 172 "repo_name": "user-group/repo-name",
173 173 "repo_type": "hg"
174 174 }
175 175 }
176 176 """
177 177
178 178 repo = get_repo_or_error(repoid)
179 179 cache = Optional.extract(cache)
180 180
181 181 include_secrets = False
182 182 if has_superadmin_permission(apiuser):
183 183 include_secrets = True
184 184 else:
185 185 # check if we have at least read permission for this repo !
186 186 _perms = (
187 187 'repository.admin', 'repository.write', 'repository.read',)
188 188 validate_repo_permissions(apiuser, repoid, repo, _perms)
189 189
190 190 permissions = []
191 191 for _user in repo.permissions():
192 192 user_data = {
193 193 'name': _user.username,
194 194 'permission': _user.permission,
195 195 'origin': get_origin(_user),
196 196 'type': "user",
197 197 }
198 198 permissions.append(user_data)
199 199
200 200 for _user_group in repo.permission_user_groups():
201 201 user_group_data = {
202 202 'name': _user_group.users_group_name,
203 203 'permission': _user_group.permission,
204 204 'origin': get_origin(_user_group),
205 205 'type': "user_group",
206 206 }
207 207 permissions.append(user_group_data)
208 208
209 209 following_users = [
210 210 user.user.get_api_data(include_secrets=include_secrets)
211 211 for user in repo.followers]
212 212
213 213 if not cache:
214 214 repo.update_commit_cache()
215 215 data = repo.get_api_data(include_secrets=include_secrets)
216 216 data['members'] = permissions # TODO: this should be deprecated soon
217 217 data['permissions'] = permissions
218 218 data['followers'] = following_users
219 219 return data
220 220
221 221
222 222 @jsonrpc_method()
223 223 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
224 224 """
225 225 Lists all existing repositories.
226 226
227 227 This command can only be run using an |authtoken| with admin rights,
228 228 or users with at least read rights to |repos|.
229 229
230 230 :param apiuser: This is filled automatically from the |authtoken|.
231 231 :type apiuser: AuthUser
232 232 :param root: specify root repository group to fetch repositories.
233 233 filters the returned repositories to be members of given root group.
234 234 :type root: Optional(None)
235 235 :param traverse: traverse given root into subrepositories. With this flag
236 236 set to False, it will only return top-level repositories from `root`.
237 237 if root is empty it will return just top-level repositories.
238 238 :type traverse: Optional(True)
239 239
240 240
241 241 Example output:
242 242
243 243 .. code-block:: bash
244 244
245 245 id : <id_given_in_input>
246 246 result: [
247 247 {
248 248 "repo_id" : "<repo_id>",
249 249 "repo_name" : "<reponame>"
250 250 "repo_type" : "<repo_type>",
251 251 "clone_uri" : "<clone_uri>",
252 252 "private": : "<bool>",
253 253 "created_on" : "<datetimecreated>",
254 254 "description" : "<description>",
255 255 "landing_rev": "<landing_rev>",
256 256 "owner": "<repo_owner>",
257 257 "fork_of": "<name_of_fork_parent>",
258 258 "enable_downloads": "<bool>",
259 259 "enable_locking": "<bool>",
260 260 "enable_statistics": "<bool>",
261 261 },
262 262 ...
263 263 ]
264 264 error: null
265 265 """
266 266
267 267 include_secrets = has_superadmin_permission(apiuser)
268 268 _perms = ('repository.read', 'repository.write', 'repository.admin',)
269 269 extras = {'user': apiuser}
270 270
271 271 root = Optional.extract(root)
272 272 traverse = Optional.extract(traverse, binary=True)
273 273
274 274 if root:
275 275 # verify parent existance, if it's empty return an error
276 276 parent = RepoGroup.get_by_group_name(root)
277 277 if not parent:
278 278 raise JSONRPCError(
279 279 'Root repository group `{}` does not exist'.format(root))
280 280
281 281 if traverse:
282 282 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
283 283 else:
284 284 repos = RepoModel().get_repos_for_root(root=parent)
285 285 else:
286 286 if traverse:
287 287 repos = RepoModel().get_all()
288 288 else:
289 289 # return just top-level
290 290 repos = RepoModel().get_repos_for_root(root=None)
291 291
292 292 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
293 293 return [repo.get_api_data(include_secrets=include_secrets)
294 294 for repo in repo_list]
295 295
296 296
297 297 @jsonrpc_method()
298 298 def get_repo_changeset(request, apiuser, repoid, revision,
299 299 details=Optional('basic')):
300 300 """
301 301 Returns information about a changeset.
302 302
303 303 Additionally parameters define the amount of details returned by
304 304 this function.
305 305
306 306 This command can only be run using an |authtoken| with admin rights,
307 307 or users with at least read rights to the |repo|.
308 308
309 309 :param apiuser: This is filled automatically from the |authtoken|.
310 310 :type apiuser: AuthUser
311 311 :param repoid: The repository name or repository id
312 312 :type repoid: str or int
313 313 :param revision: revision for which listing should be done
314 314 :type revision: str
315 315 :param details: details can be 'basic|extended|full' full gives diff
316 316 info details like the diff itself, and number of changed files etc.
317 317 :type details: Optional(str)
318 318
319 319 """
320 320 repo = get_repo_or_error(repoid)
321 321 if not has_superadmin_permission(apiuser):
322 322 _perms = (
323 323 'repository.admin', 'repository.write', 'repository.read',)
324 324 validate_repo_permissions(apiuser, repoid, repo, _perms)
325 325
326 326 changes_details = Optional.extract(details)
327 327 _changes_details_types = ['basic', 'extended', 'full']
328 328 if changes_details not in _changes_details_types:
329 329 raise JSONRPCError(
330 330 'ret_type must be one of %s' % (
331 331 ','.join(_changes_details_types)))
332 332
333 333 pre_load = ['author', 'branch', 'date', 'message', 'parents',
334 334 'status', '_commit', '_file_paths']
335 335
336 336 try:
337 337 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
338 338 except TypeError as e:
339 339 raise JSONRPCError(e.message)
340 340 _cs_json = cs.__json__()
341 341 _cs_json['diff'] = build_commit_data(cs, changes_details)
342 342 if changes_details == 'full':
343 343 _cs_json['refs'] = {
344 344 'branches': [cs.branch],
345 345 'bookmarks': getattr(cs, 'bookmarks', []),
346 346 'tags': cs.tags
347 347 }
348 348 return _cs_json
349 349
350 350
351 351 @jsonrpc_method()
352 352 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
353 353 details=Optional('basic')):
354 354 """
355 355 Returns a set of commits limited by the number starting
356 356 from the `start_rev` option.
357 357
358 358 Additional parameters define the amount of details returned by this
359 359 function.
360 360
361 361 This command can only be run using an |authtoken| with admin rights,
362 362 or users with at least read rights to |repos|.
363 363
364 364 :param apiuser: This is filled automatically from the |authtoken|.
365 365 :type apiuser: AuthUser
366 366 :param repoid: The repository name or repository ID.
367 367 :type repoid: str or int
368 368 :param start_rev: The starting revision from where to get changesets.
369 369 :type start_rev: str
370 370 :param limit: Limit the number of commits to this amount
371 371 :type limit: str or int
372 372 :param details: Set the level of detail returned. Valid option are:
373 373 ``basic``, ``extended`` and ``full``.
374 374 :type details: Optional(str)
375 375
376 376 .. note::
377 377
378 378 Setting the parameter `details` to the value ``full`` is extensive
379 379 and returns details like the diff itself, and the number
380 380 of changed files.
381 381
382 382 """
383 383 repo = get_repo_or_error(repoid)
384 384 if not has_superadmin_permission(apiuser):
385 385 _perms = (
386 386 'repository.admin', 'repository.write', 'repository.read',)
387 387 validate_repo_permissions(apiuser, repoid, repo, _perms)
388 388
389 389 changes_details = Optional.extract(details)
390 390 _changes_details_types = ['basic', 'extended', 'full']
391 391 if changes_details not in _changes_details_types:
392 392 raise JSONRPCError(
393 393 'ret_type must be one of %s' % (
394 394 ','.join(_changes_details_types)))
395 395
396 396 limit = int(limit)
397 397 pre_load = ['author', 'branch', 'date', 'message', 'parents',
398 398 'status', '_commit', '_file_paths']
399 399
400 400 vcs_repo = repo.scm_instance()
401 401 # SVN needs a special case to distinguish its index and commit id
402 402 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
403 403 start_rev = vcs_repo.commit_ids[0]
404 404
405 405 try:
406 406 commits = vcs_repo.get_commits(
407 407 start_id=start_rev, pre_load=pre_load)
408 408 except TypeError as e:
409 409 raise JSONRPCError(e.message)
410 410 except Exception:
411 411 log.exception('Fetching of commits failed')
412 412 raise JSONRPCError('Error occurred during commit fetching')
413 413
414 414 ret = []
415 415 for cnt, commit in enumerate(commits):
416 416 if cnt >= limit != -1:
417 417 break
418 418 _cs_json = commit.__json__()
419 419 _cs_json['diff'] = build_commit_data(commit, changes_details)
420 420 if changes_details == 'full':
421 421 _cs_json['refs'] = {
422 422 'branches': [commit.branch],
423 423 'bookmarks': getattr(commit, 'bookmarks', []),
424 424 'tags': commit.tags
425 425 }
426 426 ret.append(_cs_json)
427 427 return ret
428 428
429 429
430 430 @jsonrpc_method()
431 431 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
432 432 ret_type=Optional('all'), details=Optional('basic'),
433 433 max_file_bytes=Optional(None)):
434 434 """
435 435 Returns a list of nodes and children in a flat list for a given
436 436 path at given revision.
437 437
438 438 It's possible to specify ret_type to show only `files` or `dirs`.
439 439
440 440 This command can only be run using an |authtoken| with admin rights,
441 441 or users with at least read rights to |repos|.
442 442
443 443 :param apiuser: This is filled automatically from the |authtoken|.
444 444 :type apiuser: AuthUser
445 445 :param repoid: The repository name or repository ID.
446 446 :type repoid: str or int
447 447 :param revision: The revision for which listing should be done.
448 448 :type revision: str
449 449 :param root_path: The path from which to start displaying.
450 450 :type root_path: str
451 451 :param ret_type: Set the return type. Valid options are
452 452 ``all`` (default), ``files`` and ``dirs``.
453 453 :type ret_type: Optional(str)
454 454 :param details: Returns extended information about nodes, such as
455 455 md5, binary, and or content. The valid options are ``basic`` and
456 456 ``full``.
457 457 :type details: Optional(str)
458 458 :param max_file_bytes: Only return file content under this file size bytes
459 459 :type details: Optional(int)
460 460
461 461 Example output:
462 462
463 463 .. code-block:: bash
464 464
465 465 id : <id_given_in_input>
466 466 result: [
467 467 {
468 468 "name" : "<name>"
469 469 "type" : "<type>",
470 470 "binary": "<true|false>" (only in extended mode)
471 471 "md5" : "<md5 of file content>" (only in extended mode)
472 472 },
473 473 ...
474 474 ]
475 475 error: null
476 476 """
477 477
478 478 repo = get_repo_or_error(repoid)
479 479 if not has_superadmin_permission(apiuser):
480 480 _perms = (
481 481 'repository.admin', 'repository.write', 'repository.read',)
482 482 validate_repo_permissions(apiuser, repoid, repo, _perms)
483 483
484 484 ret_type = Optional.extract(ret_type)
485 485 details = Optional.extract(details)
486 486 _extended_types = ['basic', 'full']
487 487 if details not in _extended_types:
488 488 raise JSONRPCError(
489 489 'ret_type must be one of %s' % (','.join(_extended_types)))
490 490 extended_info = False
491 491 content = False
492 492 if details == 'basic':
493 493 extended_info = True
494 494
495 495 if details == 'full':
496 496 extended_info = content = True
497 497
498 498 _map = {}
499 499 try:
500 500 # check if repo is not empty by any chance, skip quicker if it is.
501 501 _scm = repo.scm_instance()
502 502 if _scm.is_empty():
503 503 return []
504 504
505 505 _d, _f = ScmModel().get_nodes(
506 506 repo, revision, root_path, flat=False,
507 507 extended_info=extended_info, content=content,
508 508 max_file_bytes=max_file_bytes)
509 509 _map = {
510 510 'all': _d + _f,
511 511 'files': _f,
512 512 'dirs': _d,
513 513 }
514 514 return _map[ret_type]
515 515 except KeyError:
516 516 raise JSONRPCError(
517 517 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
518 518 except Exception:
519 519 log.exception("Exception occurred while trying to get repo nodes")
520 520 raise JSONRPCError(
521 521 'failed to get repo: `%s` nodes' % repo.repo_name
522 522 )
523 523
524 524
525 525 @jsonrpc_method()
526 526 def get_repo_refs(request, apiuser, repoid):
527 527 """
528 528 Returns a dictionary of current references. It returns
529 529 bookmarks, branches, closed_branches, and tags for given repository
530 530
531 531 It's possible to specify ret_type to show only `files` or `dirs`.
532 532
533 533 This command can only be run using an |authtoken| with admin rights,
534 534 or users with at least read rights to |repos|.
535 535
536 536 :param apiuser: This is filled automatically from the |authtoken|.
537 537 :type apiuser: AuthUser
538 538 :param repoid: The repository name or repository ID.
539 539 :type repoid: str or int
540 540
541 541 Example output:
542 542
543 543 .. code-block:: bash
544 544
545 545 id : <id_given_in_input>
546 546 "result": {
547 547 "bookmarks": {
548 548 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
549 549 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
550 550 },
551 551 "branches": {
552 552 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
553 553 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
554 554 },
555 555 "branches_closed": {},
556 556 "tags": {
557 557 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
558 558 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
559 559 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
560 560 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
561 561 }
562 562 }
563 563 error: null
564 564 """
565 565
566 566 repo = get_repo_or_error(repoid)
567 567 if not has_superadmin_permission(apiuser):
568 568 _perms = ('repository.admin', 'repository.write', 'repository.read',)
569 569 validate_repo_permissions(apiuser, repoid, repo, _perms)
570 570
571 571 try:
572 572 # check if repo is not empty by any chance, skip quicker if it is.
573 573 vcs_instance = repo.scm_instance()
574 574 refs = vcs_instance.refs()
575 575 return refs
576 576 except Exception:
577 577 log.exception("Exception occurred while trying to get repo refs")
578 578 raise JSONRPCError(
579 579 'failed to get repo: `%s` references' % repo.repo_name
580 580 )
581 581
582 582
583 583 @jsonrpc_method()
584 584 def create_repo(
585 585 request, apiuser, repo_name, repo_type,
586 586 owner=Optional(OAttr('apiuser')),
587 587 description=Optional(''),
588 588 private=Optional(False),
589 589 clone_uri=Optional(None),
590 590 landing_rev=Optional('rev:tip'),
591 591 enable_statistics=Optional(False),
592 592 enable_locking=Optional(False),
593 593 enable_downloads=Optional(False),
594 594 copy_permissions=Optional(False)):
595 595 """
596 596 Creates a repository.
597 597
598 598 * If the repository name contains "/", repository will be created inside
599 599 a repository group or nested repository groups
600 600
601 601 For example "foo/bar/repo1" will create |repo| called "repo1" inside
602 602 group "foo/bar". You have to have permissions to access and write to
603 603 the last repository group ("bar" in this example)
604 604
605 605 This command can only be run using an |authtoken| with at least
606 606 permissions to create repositories, or write permissions to
607 607 parent repository groups.
608 608
609 609 :param apiuser: This is filled automatically from the |authtoken|.
610 610 :type apiuser: AuthUser
611 611 :param repo_name: Set the repository name.
612 612 :type repo_name: str
613 613 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
614 614 :type repo_type: str
615 615 :param owner: user_id or username
616 616 :type owner: Optional(str)
617 617 :param description: Set the repository description.
618 618 :type description: Optional(str)
619 619 :param private: set repository as private
620 620 :type private: bool
621 621 :param clone_uri: set clone_uri
622 622 :type clone_uri: str
623 623 :param landing_rev: <rev_type>:<rev>
624 624 :type landing_rev: str
625 625 :param enable_locking:
626 626 :type enable_locking: bool
627 627 :param enable_downloads:
628 628 :type enable_downloads: bool
629 629 :param enable_statistics:
630 630 :type enable_statistics: bool
631 631 :param copy_permissions: Copy permission from group in which the
632 632 repository is being created.
633 633 :type copy_permissions: bool
634 634
635 635
636 636 Example output:
637 637
638 638 .. code-block:: bash
639 639
640 640 id : <id_given_in_input>
641 641 result: {
642 642 "msg": "Created new repository `<reponame>`",
643 643 "success": true,
644 644 "task": "<celery task id or None if done sync>"
645 645 }
646 646 error: null
647 647
648 648
649 649 Example error output:
650 650
651 651 .. code-block:: bash
652 652
653 653 id : <id_given_in_input>
654 654 result : null
655 655 error : {
656 656 'failed to create repository `<repo_name>`'
657 657 }
658 658
659 659 """
660 660
661 661 owner = validate_set_owner_permissions(apiuser, owner)
662 662
663 663 description = Optional.extract(description)
664 664 copy_permissions = Optional.extract(copy_permissions)
665 665 clone_uri = Optional.extract(clone_uri)
666 666 landing_commit_ref = Optional.extract(landing_rev)
667 667
668 668 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
669 669 if isinstance(private, Optional):
670 670 private = defs.get('repo_private') or Optional.extract(private)
671 671 if isinstance(repo_type, Optional):
672 672 repo_type = defs.get('repo_type')
673 673 if isinstance(enable_statistics, Optional):
674 674 enable_statistics = defs.get('repo_enable_statistics')
675 675 if isinstance(enable_locking, Optional):
676 676 enable_locking = defs.get('repo_enable_locking')
677 677 if isinstance(enable_downloads, Optional):
678 678 enable_downloads = defs.get('repo_enable_downloads')
679 679
680 680 schema = repo_schema.RepoSchema().bind(
681 681 repo_type_options=rhodecode.BACKENDS.keys(),
682 682 # user caller
683 683 user=apiuser)
684 684
685 685 try:
686 686 schema_data = schema.deserialize(dict(
687 687 repo_name=repo_name,
688 688 repo_type=repo_type,
689 689 repo_owner=owner.username,
690 690 repo_description=description,
691 691 repo_landing_commit_ref=landing_commit_ref,
692 692 repo_clone_uri=clone_uri,
693 693 repo_private=private,
694 694 repo_copy_permissions=copy_permissions,
695 695 repo_enable_statistics=enable_statistics,
696 696 repo_enable_downloads=enable_downloads,
697 697 repo_enable_locking=enable_locking))
698 698 except validation_schema.Invalid as err:
699 699 raise JSONRPCValidationError(colander_exc=err)
700 700
701 701 try:
702 702 data = {
703 703 'owner': owner,
704 704 'repo_name': schema_data['repo_group']['repo_name_without_group'],
705 705 'repo_name_full': schema_data['repo_name'],
706 706 'repo_group': schema_data['repo_group']['repo_group_id'],
707 707 'repo_type': schema_data['repo_type'],
708 708 'repo_description': schema_data['repo_description'],
709 709 'repo_private': schema_data['repo_private'],
710 710 'clone_uri': schema_data['repo_clone_uri'],
711 711 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
712 712 'enable_statistics': schema_data['repo_enable_statistics'],
713 713 'enable_locking': schema_data['repo_enable_locking'],
714 714 'enable_downloads': schema_data['repo_enable_downloads'],
715 715 'repo_copy_permissions': schema_data['repo_copy_permissions'],
716 716 }
717 717
718 718 task = RepoModel().create(form_data=data, cur_user=owner)
719 719 from celery.result import BaseAsyncResult
720 720 task_id = None
721 721 if isinstance(task, BaseAsyncResult):
722 722 task_id = task.task_id
723 723 # no commit, it's done in RepoModel, or async via celery
724 724 return {
725 725 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
726 726 'success': True, # cannot return the repo data here since fork
727 727 # can be done async
728 728 'task': task_id
729 729 }
730 730 except Exception:
731 731 log.exception(
732 732 u"Exception while trying to create the repository %s",
733 733 schema_data['repo_name'])
734 734 raise JSONRPCError(
735 735 'failed to create repository `%s`' % (schema_data['repo_name'],))
736 736
737 737
738 738 @jsonrpc_method()
739 739 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
740 740 description=Optional('')):
741 741 """
742 742 Adds an extra field to a repository.
743 743
744 744 This command can only be run using an |authtoken| with at least
745 745 write permissions to the |repo|.
746 746
747 747 :param apiuser: This is filled automatically from the |authtoken|.
748 748 :type apiuser: AuthUser
749 749 :param repoid: Set the repository name or repository id.
750 750 :type repoid: str or int
751 751 :param key: Create a unique field key for this repository.
752 752 :type key: str
753 753 :param label:
754 754 :type label: Optional(str)
755 755 :param description:
756 756 :type description: Optional(str)
757 757 """
758 758 repo = get_repo_or_error(repoid)
759 759 if not has_superadmin_permission(apiuser):
760 760 _perms = ('repository.admin',)
761 761 validate_repo_permissions(apiuser, repoid, repo, _perms)
762 762
763 763 label = Optional.extract(label) or key
764 764 description = Optional.extract(description)
765 765
766 766 field = RepositoryField.get_by_key_name(key, repo)
767 767 if field:
768 768 raise JSONRPCError('Field with key '
769 769 '`%s` exists for repo `%s`' % (key, repoid))
770 770
771 771 try:
772 772 RepoModel().add_repo_field(repo, key, field_label=label,
773 773 field_desc=description)
774 774 Session().commit()
775 775 return {
776 776 'msg': "Added new repository field `%s`" % (key,),
777 777 'success': True,
778 778 }
779 779 except Exception:
780 780 log.exception("Exception occurred while trying to add field to repo")
781 781 raise JSONRPCError(
782 782 'failed to create new field for repository `%s`' % (repoid,))
783 783
784 784
785 785 @jsonrpc_method()
786 786 def remove_field_from_repo(request, apiuser, repoid, key):
787 787 """
788 788 Removes an extra field from a repository.
789 789
790 790 This command can only be run using an |authtoken| with at least
791 791 write permissions to the |repo|.
792 792
793 793 :param apiuser: This is filled automatically from the |authtoken|.
794 794 :type apiuser: AuthUser
795 795 :param repoid: Set the repository name or repository ID.
796 796 :type repoid: str or int
797 797 :param key: Set the unique field key for this repository.
798 798 :type key: str
799 799 """
800 800
801 801 repo = get_repo_or_error(repoid)
802 802 if not has_superadmin_permission(apiuser):
803 803 _perms = ('repository.admin',)
804 804 validate_repo_permissions(apiuser, repoid, repo, _perms)
805 805
806 806 field = RepositoryField.get_by_key_name(key, repo)
807 807 if not field:
808 808 raise JSONRPCError('Field with key `%s` does not '
809 809 'exists for repo `%s`' % (key, repoid))
810 810
811 811 try:
812 812 RepoModel().delete_repo_field(repo, field_key=key)
813 813 Session().commit()
814 814 return {
815 815 'msg': "Deleted repository field `%s`" % (key,),
816 816 'success': True,
817 817 }
818 818 except Exception:
819 819 log.exception(
820 820 "Exception occurred while trying to delete field from repo")
821 821 raise JSONRPCError(
822 822 'failed to delete field for repository `%s`' % (repoid,))
823 823
824 824
825 825 @jsonrpc_method()
826 826 def update_repo(
827 827 request, apiuser, repoid, repo_name=Optional(None),
828 828 owner=Optional(OAttr('apiuser')), description=Optional(''),
829 829 private=Optional(False), clone_uri=Optional(None),
830 830 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
831 831 enable_statistics=Optional(False),
832 832 enable_locking=Optional(False),
833 833 enable_downloads=Optional(False), fields=Optional('')):
834 834 """
835 835 Updates a repository with the given information.
836 836
837 837 This command can only be run using an |authtoken| with at least
838 838 admin permissions to the |repo|.
839 839
840 840 * If the repository name contains "/", repository will be updated
841 841 accordingly with a repository group or nested repository groups
842 842
843 843 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
844 844 called "repo-test" and place it inside group "foo/bar".
845 845 You have to have permissions to access and write to the last repository
846 846 group ("bar" in this example)
847 847
848 848 :param apiuser: This is filled automatically from the |authtoken|.
849 849 :type apiuser: AuthUser
850 850 :param repoid: repository name or repository ID.
851 851 :type repoid: str or int
852 852 :param repo_name: Update the |repo| name, including the
853 853 repository group it's in.
854 854 :type repo_name: str
855 855 :param owner: Set the |repo| owner.
856 856 :type owner: str
857 857 :param fork_of: Set the |repo| as fork of another |repo|.
858 858 :type fork_of: str
859 859 :param description: Update the |repo| description.
860 860 :type description: str
861 861 :param private: Set the |repo| as private. (True | False)
862 862 :type private: bool
863 863 :param clone_uri: Update the |repo| clone URI.
864 864 :type clone_uri: str
865 865 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
866 866 :type landing_rev: str
867 867 :param enable_statistics: Enable statistics on the |repo|, (True | False).
868 868 :type enable_statistics: bool
869 869 :param enable_locking: Enable |repo| locking.
870 870 :type enable_locking: bool
871 871 :param enable_downloads: Enable downloads from the |repo|, (True | False).
872 872 :type enable_downloads: bool
873 873 :param fields: Add extra fields to the |repo|. Use the following
874 874 example format: ``field_key=field_val,field_key2=fieldval2``.
875 875 Escape ', ' with \,
876 876 :type fields: str
877 877 """
878 878
879 879 repo = get_repo_or_error(repoid)
880 880
881 881 include_secrets = False
882 882 if not has_superadmin_permission(apiuser):
883 883 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
884 884 else:
885 885 include_secrets = True
886 886
887 887 updates = dict(
888 888 repo_name=repo_name
889 889 if not isinstance(repo_name, Optional) else repo.repo_name,
890 890
891 891 fork_id=fork_of
892 892 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
893 893
894 894 user=owner
895 895 if not isinstance(owner, Optional) else repo.user.username,
896 896
897 897 repo_description=description
898 898 if not isinstance(description, Optional) else repo.description,
899 899
900 900 repo_private=private
901 901 if not isinstance(private, Optional) else repo.private,
902 902
903 903 clone_uri=clone_uri
904 904 if not isinstance(clone_uri, Optional) else repo.clone_uri,
905 905
906 906 repo_landing_rev=landing_rev
907 907 if not isinstance(landing_rev, Optional) else repo._landing_revision,
908 908
909 909 repo_enable_statistics=enable_statistics
910 910 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
911 911
912 912 repo_enable_locking=enable_locking
913 913 if not isinstance(enable_locking, Optional) else repo.enable_locking,
914 914
915 915 repo_enable_downloads=enable_downloads
916 916 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
917 917
918 918 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
919 919
920 old_values = repo.get_api_data()
920 921 schema = repo_schema.RepoSchema().bind(
921 922 repo_type_options=rhodecode.BACKENDS.keys(),
922 923 repo_ref_options=ref_choices,
923 924 # user caller
924 925 user=apiuser,
925 old_values=repo.get_api_data())
926 old_values=old_values)
926 927 try:
927 928 schema_data = schema.deserialize(dict(
928 929 # we save old value, users cannot change type
929 930 repo_type=repo.repo_type,
930 931
931 932 repo_name=updates['repo_name'],
932 933 repo_owner=updates['user'],
933 934 repo_description=updates['repo_description'],
934 935 repo_clone_uri=updates['clone_uri'],
935 936 repo_fork_of=updates['fork_id'],
936 937 repo_private=updates['repo_private'],
937 938 repo_landing_commit_ref=updates['repo_landing_rev'],
938 939 repo_enable_statistics=updates['repo_enable_statistics'],
939 940 repo_enable_downloads=updates['repo_enable_downloads'],
940 941 repo_enable_locking=updates['repo_enable_locking']))
941 942 except validation_schema.Invalid as err:
942 943 raise JSONRPCValidationError(colander_exc=err)
943 944
944 945 # save validated data back into the updates dict
945 946 validated_updates = dict(
946 947 repo_name=schema_data['repo_group']['repo_name_without_group'],
947 948 repo_group=schema_data['repo_group']['repo_group_id'],
948 949
949 950 user=schema_data['repo_owner'],
950 951 repo_description=schema_data['repo_description'],
951 952 repo_private=schema_data['repo_private'],
952 953 clone_uri=schema_data['repo_clone_uri'],
953 954 repo_landing_rev=schema_data['repo_landing_commit_ref'],
954 955 repo_enable_statistics=schema_data['repo_enable_statistics'],
955 956 repo_enable_locking=schema_data['repo_enable_locking'],
956 957 repo_enable_downloads=schema_data['repo_enable_downloads'],
957 958 )
958 959
959 960 if schema_data['repo_fork_of']:
960 961 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
961 962 validated_updates['fork_id'] = fork_repo.repo_id
962 963
963 964 # extra fields
964 965 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
965 966 if fields:
966 967 validated_updates.update(fields)
967 968
968 969 try:
969 970 RepoModel().update(repo, **validated_updates)
971 audit_logger.store_api(
972 'repo.edit', action_data={'old_data': old_values},
973 user=apiuser, repo=repo)
970 974 Session().commit()
971 975 return {
972 976 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
973 977 'repository': repo.get_api_data(include_secrets=include_secrets)
974 978 }
975 979 except Exception:
976 980 log.exception(
977 981 u"Exception while trying to update the repository %s",
978 982 repoid)
979 983 raise JSONRPCError('failed to update repo `%s`' % repoid)
980 984
981 985
982 986 @jsonrpc_method()
983 987 def fork_repo(request, apiuser, repoid, fork_name,
984 988 owner=Optional(OAttr('apiuser')),
985 989 description=Optional(''),
986 990 private=Optional(False),
987 991 clone_uri=Optional(None),
988 992 landing_rev=Optional('rev:tip'),
989 993 copy_permissions=Optional(False)):
990 994 """
991 995 Creates a fork of the specified |repo|.
992 996
993 997 * If the fork_name contains "/", fork will be created inside
994 998 a repository group or nested repository groups
995 999
996 1000 For example "foo/bar/fork-repo" will create fork called "fork-repo"
997 1001 inside group "foo/bar". You have to have permissions to access and
998 1002 write to the last repository group ("bar" in this example)
999 1003
1000 1004 This command can only be run using an |authtoken| with minimum
1001 1005 read permissions of the forked repo, create fork permissions for an user.
1002 1006
1003 1007 :param apiuser: This is filled automatically from the |authtoken|.
1004 1008 :type apiuser: AuthUser
1005 1009 :param repoid: Set repository name or repository ID.
1006 1010 :type repoid: str or int
1007 1011 :param fork_name: Set the fork name, including it's repository group membership.
1008 1012 :type fork_name: str
1009 1013 :param owner: Set the fork owner.
1010 1014 :type owner: str
1011 1015 :param description: Set the fork description.
1012 1016 :type description: str
1013 1017 :param copy_permissions: Copy permissions from parent |repo|. The
1014 1018 default is False.
1015 1019 :type copy_permissions: bool
1016 1020 :param private: Make the fork private. The default is False.
1017 1021 :type private: bool
1018 1022 :param landing_rev: Set the landing revision. The default is tip.
1019 1023
1020 1024 Example output:
1021 1025
1022 1026 .. code-block:: bash
1023 1027
1024 1028 id : <id_for_response>
1025 1029 api_key : "<api_key>"
1026 1030 args: {
1027 1031 "repoid" : "<reponame or repo_id>",
1028 1032 "fork_name": "<forkname>",
1029 1033 "owner": "<username or user_id = Optional(=apiuser)>",
1030 1034 "description": "<description>",
1031 1035 "copy_permissions": "<bool>",
1032 1036 "private": "<bool>",
1033 1037 "landing_rev": "<landing_rev>"
1034 1038 }
1035 1039
1036 1040 Example error output:
1037 1041
1038 1042 .. code-block:: bash
1039 1043
1040 1044 id : <id_given_in_input>
1041 1045 result: {
1042 1046 "msg": "Created fork of `<reponame>` as `<forkname>`",
1043 1047 "success": true,
1044 1048 "task": "<celery task id or None if done sync>"
1045 1049 }
1046 1050 error: null
1047 1051
1048 1052 """
1049 1053
1050 1054 repo = get_repo_or_error(repoid)
1051 1055 repo_name = repo.repo_name
1052 1056
1053 1057 if not has_superadmin_permission(apiuser):
1054 1058 # check if we have at least read permission for
1055 1059 # this repo that we fork !
1056 1060 _perms = (
1057 1061 'repository.admin', 'repository.write', 'repository.read')
1058 1062 validate_repo_permissions(apiuser, repoid, repo, _perms)
1059 1063
1060 1064 # check if the regular user has at least fork permissions as well
1061 1065 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1062 1066 raise JSONRPCForbidden()
1063 1067
1064 1068 # check if user can set owner parameter
1065 1069 owner = validate_set_owner_permissions(apiuser, owner)
1066 1070
1067 1071 description = Optional.extract(description)
1068 1072 copy_permissions = Optional.extract(copy_permissions)
1069 1073 clone_uri = Optional.extract(clone_uri)
1070 1074 landing_commit_ref = Optional.extract(landing_rev)
1071 1075 private = Optional.extract(private)
1072 1076
1073 1077 schema = repo_schema.RepoSchema().bind(
1074 1078 repo_type_options=rhodecode.BACKENDS.keys(),
1075 1079 # user caller
1076 1080 user=apiuser)
1077 1081
1078 1082 try:
1079 1083 schema_data = schema.deserialize(dict(
1080 1084 repo_name=fork_name,
1081 1085 repo_type=repo.repo_type,
1082 1086 repo_owner=owner.username,
1083 1087 repo_description=description,
1084 1088 repo_landing_commit_ref=landing_commit_ref,
1085 1089 repo_clone_uri=clone_uri,
1086 1090 repo_private=private,
1087 1091 repo_copy_permissions=copy_permissions))
1088 1092 except validation_schema.Invalid as err:
1089 1093 raise JSONRPCValidationError(colander_exc=err)
1090 1094
1091 1095 try:
1092 1096 data = {
1093 1097 'fork_parent_id': repo.repo_id,
1094 1098
1095 1099 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1096 1100 'repo_name_full': schema_data['repo_name'],
1097 1101 'repo_group': schema_data['repo_group']['repo_group_id'],
1098 1102 'repo_type': schema_data['repo_type'],
1099 1103 'description': schema_data['repo_description'],
1100 1104 'private': schema_data['repo_private'],
1101 1105 'copy_permissions': schema_data['repo_copy_permissions'],
1102 1106 'landing_rev': schema_data['repo_landing_commit_ref'],
1103 1107 }
1104 1108
1105 1109 task = RepoModel().create_fork(data, cur_user=owner)
1106 1110 # no commit, it's done in RepoModel, or async via celery
1107 1111 from celery.result import BaseAsyncResult
1108 1112 task_id = None
1109 1113 if isinstance(task, BaseAsyncResult):
1110 1114 task_id = task.task_id
1111 1115 return {
1112 1116 'msg': 'Created fork of `%s` as `%s`' % (
1113 1117 repo.repo_name, schema_data['repo_name']),
1114 1118 'success': True, # cannot return the repo data here since fork
1115 1119 # can be done async
1116 1120 'task': task_id
1117 1121 }
1118 1122 except Exception:
1119 1123 log.exception(
1120 1124 u"Exception while trying to create fork %s",
1121 1125 schema_data['repo_name'])
1122 1126 raise JSONRPCError(
1123 1127 'failed to fork repository `%s` as `%s`' % (
1124 1128 repo_name, schema_data['repo_name']))
1125 1129
1126 1130
1127 1131 @jsonrpc_method()
1128 1132 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1129 1133 """
1130 1134 Deletes a repository.
1131 1135
1132 1136 * When the `forks` parameter is set it's possible to detach or delete
1133 1137 forks of deleted repository.
1134 1138
1135 1139 This command can only be run using an |authtoken| with admin
1136 1140 permissions on the |repo|.
1137 1141
1138 1142 :param apiuser: This is filled automatically from the |authtoken|.
1139 1143 :type apiuser: AuthUser
1140 1144 :param repoid: Set the repository name or repository ID.
1141 1145 :type repoid: str or int
1142 1146 :param forks: Set to `detach` or `delete` forks from the |repo|.
1143 1147 :type forks: Optional(str)
1144 1148
1145 1149 Example error output:
1146 1150
1147 1151 .. code-block:: bash
1148 1152
1149 1153 id : <id_given_in_input>
1150 1154 result: {
1151 1155 "msg": "Deleted repository `<reponame>`",
1152 1156 "success": true
1153 1157 }
1154 1158 error: null
1155 1159 """
1156 1160
1157 1161 repo = get_repo_or_error(repoid)
1158 1162 repo_name = repo.repo_name
1159 1163 if not has_superadmin_permission(apiuser):
1160 1164 _perms = ('repository.admin',)
1161 1165 validate_repo_permissions(apiuser, repoid, repo, _perms)
1162 1166
1163 1167 try:
1164 1168 handle_forks = Optional.extract(forks)
1165 1169 _forks_msg = ''
1166 1170 _forks = [f for f in repo.forks]
1167 1171 if handle_forks == 'detach':
1168 1172 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1169 1173 elif handle_forks == 'delete':
1170 1174 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1171 1175 elif _forks:
1172 1176 raise JSONRPCError(
1173 1177 'Cannot delete `%s` it still contains attached forks' %
1174 1178 (repo.repo_name,)
1175 1179 )
1176 repo_data = repo.get_api_data()
1180 old_data = repo.get_api_data()
1177 1181 RepoModel().delete(repo, forks=forks)
1178 1182
1179 1183 repo = audit_logger.RepoWrap(repo_id=None,
1180 1184 repo_name=repo.repo_name)
1181 1185
1182 1186 audit_logger.store_api(
1183 action='repo.delete',
1184 action_data={'data': repo_data},
1187 'repo.delete', action_data={'old_data': old_data},
1185 1188 user=apiuser, repo=repo)
1186 1189
1187 1190 ScmModel().mark_for_invalidation(repo_name, delete=True)
1188 1191 Session().commit()
1189 1192 return {
1190 1193 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1191 1194 'success': True
1192 1195 }
1193 1196 except Exception:
1194 1197 log.exception("Exception occurred while trying to delete repo")
1195 1198 raise JSONRPCError(
1196 1199 'failed to delete repository `%s`' % (repo_name,)
1197 1200 )
1198 1201
1199 1202
1200 1203 #TODO: marcink, change name ?
1201 1204 @jsonrpc_method()
1202 1205 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1203 1206 """
1204 1207 Invalidates the cache for the specified repository.
1205 1208
1206 1209 This command can only be run using an |authtoken| with admin rights to
1207 1210 the specified repository.
1208 1211
1209 1212 This command takes the following options:
1210 1213
1211 1214 :param apiuser: This is filled automatically from |authtoken|.
1212 1215 :type apiuser: AuthUser
1213 1216 :param repoid: Sets the repository name or repository ID.
1214 1217 :type repoid: str or int
1215 1218 :param delete_keys: This deletes the invalidated keys instead of
1216 1219 just flagging them.
1217 1220 :type delete_keys: Optional(``True`` | ``False``)
1218 1221
1219 1222 Example output:
1220 1223
1221 1224 .. code-block:: bash
1222 1225
1223 1226 id : <id_given_in_input>
1224 1227 result : {
1225 1228 'msg': Cache for repository `<repository name>` was invalidated,
1226 1229 'repository': <repository name>
1227 1230 }
1228 1231 error : null
1229 1232
1230 1233 Example error output:
1231 1234
1232 1235 .. code-block:: bash
1233 1236
1234 1237 id : <id_given_in_input>
1235 1238 result : null
1236 1239 error : {
1237 1240 'Error occurred during cache invalidation action'
1238 1241 }
1239 1242
1240 1243 """
1241 1244
1242 1245 repo = get_repo_or_error(repoid)
1243 1246 if not has_superadmin_permission(apiuser):
1244 1247 _perms = ('repository.admin', 'repository.write',)
1245 1248 validate_repo_permissions(apiuser, repoid, repo, _perms)
1246 1249
1247 1250 delete = Optional.extract(delete_keys)
1248 1251 try:
1249 1252 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1250 1253 return {
1251 1254 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1252 1255 'repository': repo.repo_name
1253 1256 }
1254 1257 except Exception:
1255 1258 log.exception(
1256 1259 "Exception occurred while trying to invalidate repo cache")
1257 1260 raise JSONRPCError(
1258 1261 'Error occurred during cache invalidation action'
1259 1262 )
1260 1263
1261 1264
1262 1265 #TODO: marcink, change name ?
1263 1266 @jsonrpc_method()
1264 1267 def lock(request, apiuser, repoid, locked=Optional(None),
1265 1268 userid=Optional(OAttr('apiuser'))):
1266 1269 """
1267 1270 Sets the lock state of the specified |repo| by the given user.
1268 1271 From more information, see :ref:`repo-locking`.
1269 1272
1270 1273 * If the ``userid`` option is not set, the repository is locked to the
1271 1274 user who called the method.
1272 1275 * If the ``locked`` parameter is not set, the current lock state of the
1273 1276 repository is displayed.
1274 1277
1275 1278 This command can only be run using an |authtoken| with admin rights to
1276 1279 the specified repository.
1277 1280
1278 1281 This command takes the following options:
1279 1282
1280 1283 :param apiuser: This is filled automatically from the |authtoken|.
1281 1284 :type apiuser: AuthUser
1282 1285 :param repoid: Sets the repository name or repository ID.
1283 1286 :type repoid: str or int
1284 1287 :param locked: Sets the lock state.
1285 1288 :type locked: Optional(``True`` | ``False``)
1286 1289 :param userid: Set the repository lock to this user.
1287 1290 :type userid: Optional(str or int)
1288 1291
1289 1292 Example error output:
1290 1293
1291 1294 .. code-block:: bash
1292 1295
1293 1296 id : <id_given_in_input>
1294 1297 result : {
1295 1298 'repo': '<reponame>',
1296 1299 'locked': <bool: lock state>,
1297 1300 'locked_since': <int: lock timestamp>,
1298 1301 'locked_by': <username of person who made the lock>,
1299 1302 'lock_reason': <str: reason for locking>,
1300 1303 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1301 1304 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1302 1305 or
1303 1306 'msg': 'Repo `<repository name>` not locked.'
1304 1307 or
1305 1308 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1306 1309 }
1307 1310 error : null
1308 1311
1309 1312 Example error output:
1310 1313
1311 1314 .. code-block:: bash
1312 1315
1313 1316 id : <id_given_in_input>
1314 1317 result : null
1315 1318 error : {
1316 1319 'Error occurred locking repository `<reponame>`'
1317 1320 }
1318 1321 """
1319 1322
1320 1323 repo = get_repo_or_error(repoid)
1321 1324 if not has_superadmin_permission(apiuser):
1322 1325 # check if we have at least write permission for this repo !
1323 1326 _perms = ('repository.admin', 'repository.write',)
1324 1327 validate_repo_permissions(apiuser, repoid, repo, _perms)
1325 1328
1326 1329 # make sure normal user does not pass someone else userid,
1327 1330 # he is not allowed to do that
1328 1331 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1329 1332 raise JSONRPCError('userid is not the same as your user')
1330 1333
1331 1334 if isinstance(userid, Optional):
1332 1335 userid = apiuser.user_id
1333 1336
1334 1337 user = get_user_or_error(userid)
1335 1338
1336 1339 if isinstance(locked, Optional):
1337 1340 lockobj = repo.locked
1338 1341
1339 1342 if lockobj[0] is None:
1340 1343 _d = {
1341 1344 'repo': repo.repo_name,
1342 1345 'locked': False,
1343 1346 'locked_since': None,
1344 1347 'locked_by': None,
1345 1348 'lock_reason': None,
1346 1349 'lock_state_changed': False,
1347 1350 'msg': 'Repo `%s` not locked.' % repo.repo_name
1348 1351 }
1349 1352 return _d
1350 1353 else:
1351 1354 _user_id, _time, _reason = lockobj
1352 1355 lock_user = get_user_or_error(userid)
1353 1356 _d = {
1354 1357 'repo': repo.repo_name,
1355 1358 'locked': True,
1356 1359 'locked_since': _time,
1357 1360 'locked_by': lock_user.username,
1358 1361 'lock_reason': _reason,
1359 1362 'lock_state_changed': False,
1360 1363 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1361 1364 % (repo.repo_name, lock_user.username,
1362 1365 json.dumps(time_to_datetime(_time))))
1363 1366 }
1364 1367 return _d
1365 1368
1366 1369 # force locked state through a flag
1367 1370 else:
1368 1371 locked = str2bool(locked)
1369 1372 lock_reason = Repository.LOCK_API
1370 1373 try:
1371 1374 if locked:
1372 1375 lock_time = time.time()
1373 1376 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1374 1377 else:
1375 1378 lock_time = None
1376 1379 Repository.unlock(repo)
1377 1380 _d = {
1378 1381 'repo': repo.repo_name,
1379 1382 'locked': locked,
1380 1383 'locked_since': lock_time,
1381 1384 'locked_by': user.username,
1382 1385 'lock_reason': lock_reason,
1383 1386 'lock_state_changed': True,
1384 1387 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1385 1388 % (user.username, repo.repo_name, locked))
1386 1389 }
1387 1390 return _d
1388 1391 except Exception:
1389 1392 log.exception(
1390 1393 "Exception occurred while trying to lock repository")
1391 1394 raise JSONRPCError(
1392 1395 'Error occurred locking repository `%s`' % repo.repo_name
1393 1396 )
1394 1397
1395 1398
1396 1399 @jsonrpc_method()
1397 1400 def comment_commit(
1398 1401 request, apiuser, repoid, commit_id, message, status=Optional(None),
1399 1402 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1400 1403 resolves_comment_id=Optional(None),
1401 1404 userid=Optional(OAttr('apiuser'))):
1402 1405 """
1403 1406 Set a commit comment, and optionally change the status of the commit.
1404 1407
1405 1408 :param apiuser: This is filled automatically from the |authtoken|.
1406 1409 :type apiuser: AuthUser
1407 1410 :param repoid: Set the repository name or repository ID.
1408 1411 :type repoid: str or int
1409 1412 :param commit_id: Specify the commit_id for which to set a comment.
1410 1413 :type commit_id: str
1411 1414 :param message: The comment text.
1412 1415 :type message: str
1413 1416 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1414 1417 'approved', 'rejected', 'under_review'
1415 1418 :type status: str
1416 1419 :param comment_type: Comment type, one of: 'note', 'todo'
1417 1420 :type comment_type: Optional(str), default: 'note'
1418 1421 :param userid: Set the user name of the comment creator.
1419 1422 :type userid: Optional(str or int)
1420 1423
1421 1424 Example error output:
1422 1425
1423 1426 .. code-block:: bash
1424 1427
1425 1428 {
1426 1429 "id" : <id_given_in_input>,
1427 1430 "result" : {
1428 1431 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1429 1432 "status_change": null or <status>,
1430 1433 "success": true
1431 1434 },
1432 1435 "error" : null
1433 1436 }
1434 1437
1435 1438 """
1436 1439 repo = get_repo_or_error(repoid)
1437 1440 if not has_superadmin_permission(apiuser):
1438 1441 _perms = ('repository.read', 'repository.write', 'repository.admin')
1439 1442 validate_repo_permissions(apiuser, repoid, repo, _perms)
1440 1443
1441 1444 try:
1442 1445 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1443 1446 except Exception as e:
1444 1447 log.exception('Failed to fetch commit')
1445 1448 raise JSONRPCError(e.message)
1446 1449
1447 1450 if isinstance(userid, Optional):
1448 1451 userid = apiuser.user_id
1449 1452
1450 1453 user = get_user_or_error(userid)
1451 1454 status = Optional.extract(status)
1452 1455 comment_type = Optional.extract(comment_type)
1453 1456 resolves_comment_id = Optional.extract(resolves_comment_id)
1454 1457
1455 1458 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1456 1459 if status and status not in allowed_statuses:
1457 1460 raise JSONRPCError('Bad status, must be on '
1458 1461 'of %s got %s' % (allowed_statuses, status,))
1459 1462
1460 1463 if resolves_comment_id:
1461 1464 comment = ChangesetComment.get(resolves_comment_id)
1462 1465 if not comment:
1463 1466 raise JSONRPCError(
1464 1467 'Invalid resolves_comment_id `%s` for this commit.'
1465 1468 % resolves_comment_id)
1466 1469 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1467 1470 raise JSONRPCError(
1468 1471 'Comment `%s` is wrong type for setting status to resolved.'
1469 1472 % resolves_comment_id)
1470 1473
1471 1474 try:
1472 1475 rc_config = SettingsModel().get_all_settings()
1473 1476 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1474 1477 status_change_label = ChangesetStatus.get_status_lbl(status)
1475 comm = CommentsModel().create(
1478 comment = CommentsModel().create(
1476 1479 message, repo, user, commit_id=commit_id,
1477 1480 status_change=status_change_label,
1478 1481 status_change_type=status,
1479 1482 renderer=renderer,
1480 1483 comment_type=comment_type,
1481 1484 resolves_comment_id=resolves_comment_id
1482 1485 )
1483 1486 if status:
1484 1487 # also do a status change
1485 1488 try:
1486 1489 ChangesetStatusModel().set_status(
1487 repo, status, user, comm, revision=commit_id,
1490 repo, status, user, comment, revision=commit_id,
1488 1491 dont_allow_on_closed_pull_request=True
1489 1492 )
1490 1493 except StatusChangeOnClosedPullRequestError:
1491 1494 log.exception(
1492 1495 "Exception occurred while trying to change repo commit status")
1493 1496 msg = ('Changing status on a changeset associated with '
1494 1497 'a closed pull request is not allowed')
1495 1498 raise JSONRPCError(msg)
1496 1499
1497 1500 Session().commit()
1498 1501 return {
1499 1502 'msg': (
1500 1503 'Commented on commit `%s` for repository `%s`' % (
1501 comm.revision, repo.repo_name)),
1504 comment.revision, repo.repo_name)),
1502 1505 'status_change': status,
1503 1506 'success': True,
1504 1507 }
1505 1508 except JSONRPCError:
1506 1509 # catch any inside errors, and re-raise them to prevent from
1507 1510 # below global catch to silence them
1508 1511 raise
1509 1512 except Exception:
1510 1513 log.exception("Exception occurred while trying to comment on commit")
1511 1514 raise JSONRPCError(
1512 1515 'failed to set comment on repository `%s`' % (repo.repo_name,)
1513 1516 )
1514 1517
1515 1518
1516 1519 @jsonrpc_method()
1517 1520 def grant_user_permission(request, apiuser, repoid, userid, perm):
1518 1521 """
1519 1522 Grant permissions for the specified user on the given repository,
1520 1523 or update existing permissions if found.
1521 1524
1522 1525 This command can only be run using an |authtoken| with admin
1523 1526 permissions on the |repo|.
1524 1527
1525 1528 :param apiuser: This is filled automatically from the |authtoken|.
1526 1529 :type apiuser: AuthUser
1527 1530 :param repoid: Set the repository name or repository ID.
1528 1531 :type repoid: str or int
1529 1532 :param userid: Set the user name.
1530 1533 :type userid: str
1531 1534 :param perm: Set the user permissions, using the following format
1532 1535 ``(repository.(none|read|write|admin))``
1533 1536 :type perm: str
1534 1537
1535 1538 Example output:
1536 1539
1537 1540 .. code-block:: bash
1538 1541
1539 1542 id : <id_given_in_input>
1540 1543 result: {
1541 1544 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1542 1545 "success": true
1543 1546 }
1544 1547 error: null
1545 1548 """
1546 1549
1547 1550 repo = get_repo_or_error(repoid)
1548 1551 user = get_user_or_error(userid)
1549 1552 perm = get_perm_or_error(perm)
1550 1553 if not has_superadmin_permission(apiuser):
1551 1554 _perms = ('repository.admin',)
1552 1555 validate_repo_permissions(apiuser, repoid, repo, _perms)
1553 1556
1554 1557 try:
1555 1558
1556 1559 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1557 1560
1558 1561 Session().commit()
1559 1562 return {
1560 1563 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1561 1564 perm.permission_name, user.username, repo.repo_name
1562 1565 ),
1563 1566 'success': True
1564 1567 }
1565 1568 except Exception:
1566 1569 log.exception(
1567 1570 "Exception occurred while trying edit permissions for repo")
1568 1571 raise JSONRPCError(
1569 1572 'failed to edit permission for user: `%s` in repo: `%s`' % (
1570 1573 userid, repoid
1571 1574 )
1572 1575 )
1573 1576
1574 1577
1575 1578 @jsonrpc_method()
1576 1579 def revoke_user_permission(request, apiuser, repoid, userid):
1577 1580 """
1578 1581 Revoke permission for a user on the specified repository.
1579 1582
1580 1583 This command can only be run using an |authtoken| with admin
1581 1584 permissions on the |repo|.
1582 1585
1583 1586 :param apiuser: This is filled automatically from the |authtoken|.
1584 1587 :type apiuser: AuthUser
1585 1588 :param repoid: Set the repository name or repository ID.
1586 1589 :type repoid: str or int
1587 1590 :param userid: Set the user name of revoked user.
1588 1591 :type userid: str or int
1589 1592
1590 1593 Example error output:
1591 1594
1592 1595 .. code-block:: bash
1593 1596
1594 1597 id : <id_given_in_input>
1595 1598 result: {
1596 1599 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1597 1600 "success": true
1598 1601 }
1599 1602 error: null
1600 1603 """
1601 1604
1602 1605 repo = get_repo_or_error(repoid)
1603 1606 user = get_user_or_error(userid)
1604 1607 if not has_superadmin_permission(apiuser):
1605 1608 _perms = ('repository.admin',)
1606 1609 validate_repo_permissions(apiuser, repoid, repo, _perms)
1607 1610
1608 1611 try:
1609 1612 RepoModel().revoke_user_permission(repo=repo, user=user)
1610 1613 Session().commit()
1611 1614 return {
1612 1615 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1613 1616 user.username, repo.repo_name
1614 1617 ),
1615 1618 'success': True
1616 1619 }
1617 1620 except Exception:
1618 1621 log.exception(
1619 1622 "Exception occurred while trying revoke permissions to repo")
1620 1623 raise JSONRPCError(
1621 1624 'failed to edit permission for user: `%s` in repo: `%s`' % (
1622 1625 userid, repoid
1623 1626 )
1624 1627 )
1625 1628
1626 1629
1627 1630 @jsonrpc_method()
1628 1631 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1629 1632 """
1630 1633 Grant permission for a user group on the specified repository,
1631 1634 or update existing permissions.
1632 1635
1633 1636 This command can only be run using an |authtoken| with admin
1634 1637 permissions on the |repo|.
1635 1638
1636 1639 :param apiuser: This is filled automatically from the |authtoken|.
1637 1640 :type apiuser: AuthUser
1638 1641 :param repoid: Set the repository name or repository ID.
1639 1642 :type repoid: str or int
1640 1643 :param usergroupid: Specify the ID of the user group.
1641 1644 :type usergroupid: str or int
1642 1645 :param perm: Set the user group permissions using the following
1643 1646 format: (repository.(none|read|write|admin))
1644 1647 :type perm: str
1645 1648
1646 1649 Example output:
1647 1650
1648 1651 .. code-block:: bash
1649 1652
1650 1653 id : <id_given_in_input>
1651 1654 result : {
1652 1655 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1653 1656 "success": true
1654 1657
1655 1658 }
1656 1659 error : null
1657 1660
1658 1661 Example error output:
1659 1662
1660 1663 .. code-block:: bash
1661 1664
1662 1665 id : <id_given_in_input>
1663 1666 result : null
1664 1667 error : {
1665 1668 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1666 1669 }
1667 1670
1668 1671 """
1669 1672
1670 1673 repo = get_repo_or_error(repoid)
1671 1674 perm = get_perm_or_error(perm)
1672 1675 if not has_superadmin_permission(apiuser):
1673 1676 _perms = ('repository.admin',)
1674 1677 validate_repo_permissions(apiuser, repoid, repo, _perms)
1675 1678
1676 1679 user_group = get_user_group_or_error(usergroupid)
1677 1680 if not has_superadmin_permission(apiuser):
1678 1681 # check if we have at least read permission for this user group !
1679 1682 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1680 1683 if not HasUserGroupPermissionAnyApi(*_perms)(
1681 1684 user=apiuser, user_group_name=user_group.users_group_name):
1682 1685 raise JSONRPCError(
1683 1686 'user group `%s` does not exist' % (usergroupid,))
1684 1687
1685 1688 try:
1686 1689 RepoModel().grant_user_group_permission(
1687 1690 repo=repo, group_name=user_group, perm=perm)
1688 1691
1689 1692 Session().commit()
1690 1693 return {
1691 1694 'msg': 'Granted perm: `%s` for user group: `%s` in '
1692 1695 'repo: `%s`' % (
1693 1696 perm.permission_name, user_group.users_group_name,
1694 1697 repo.repo_name
1695 1698 ),
1696 1699 'success': True
1697 1700 }
1698 1701 except Exception:
1699 1702 log.exception(
1700 1703 "Exception occurred while trying change permission on repo")
1701 1704 raise JSONRPCError(
1702 1705 'failed to edit permission for user group: `%s` in '
1703 1706 'repo: `%s`' % (
1704 1707 usergroupid, repo.repo_name
1705 1708 )
1706 1709 )
1707 1710
1708 1711
1709 1712 @jsonrpc_method()
1710 1713 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1711 1714 """
1712 1715 Revoke the permissions of a user group on a given repository.
1713 1716
1714 1717 This command can only be run using an |authtoken| with admin
1715 1718 permissions on the |repo|.
1716 1719
1717 1720 :param apiuser: This is filled automatically from the |authtoken|.
1718 1721 :type apiuser: AuthUser
1719 1722 :param repoid: Set the repository name or repository ID.
1720 1723 :type repoid: str or int
1721 1724 :param usergroupid: Specify the user group ID.
1722 1725 :type usergroupid: str or int
1723 1726
1724 1727 Example output:
1725 1728
1726 1729 .. code-block:: bash
1727 1730
1728 1731 id : <id_given_in_input>
1729 1732 result: {
1730 1733 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1731 1734 "success": true
1732 1735 }
1733 1736 error: null
1734 1737 """
1735 1738
1736 1739 repo = get_repo_or_error(repoid)
1737 1740 if not has_superadmin_permission(apiuser):
1738 1741 _perms = ('repository.admin',)
1739 1742 validate_repo_permissions(apiuser, repoid, repo, _perms)
1740 1743
1741 1744 user_group = get_user_group_or_error(usergroupid)
1742 1745 if not has_superadmin_permission(apiuser):
1743 1746 # check if we have at least read permission for this user group !
1744 1747 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1745 1748 if not HasUserGroupPermissionAnyApi(*_perms)(
1746 1749 user=apiuser, user_group_name=user_group.users_group_name):
1747 1750 raise JSONRPCError(
1748 1751 'user group `%s` does not exist' % (usergroupid,))
1749 1752
1750 1753 try:
1751 1754 RepoModel().revoke_user_group_permission(
1752 1755 repo=repo, group_name=user_group)
1753 1756
1754 1757 Session().commit()
1755 1758 return {
1756 1759 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1757 1760 user_group.users_group_name, repo.repo_name
1758 1761 ),
1759 1762 'success': True
1760 1763 }
1761 1764 except Exception:
1762 1765 log.exception("Exception occurred while trying revoke "
1763 1766 "user group permission on repo")
1764 1767 raise JSONRPCError(
1765 1768 'failed to edit permission for user group: `%s` in '
1766 1769 'repo: `%s`' % (
1767 1770 user_group.users_group_name, repo.repo_name
1768 1771 )
1769 1772 )
1770 1773
1771 1774
1772 1775 @jsonrpc_method()
1773 1776 def pull(request, apiuser, repoid):
1774 1777 """
1775 1778 Triggers a pull on the given repository from a remote location. You
1776 1779 can use this to keep remote repositories up-to-date.
1777 1780
1778 1781 This command can only be run using an |authtoken| with admin
1779 1782 rights to the specified repository. For more information,
1780 1783 see :ref:`config-token-ref`.
1781 1784
1782 1785 This command takes the following options:
1783 1786
1784 1787 :param apiuser: This is filled automatically from the |authtoken|.
1785 1788 :type apiuser: AuthUser
1786 1789 :param repoid: The repository name or repository ID.
1787 1790 :type repoid: str or int
1788 1791
1789 1792 Example output:
1790 1793
1791 1794 .. code-block:: bash
1792 1795
1793 1796 id : <id_given_in_input>
1794 1797 result : {
1795 1798 "msg": "Pulled from `<repository name>`"
1796 1799 "repository": "<repository name>"
1797 1800 }
1798 1801 error : null
1799 1802
1800 1803 Example error output:
1801 1804
1802 1805 .. code-block:: bash
1803 1806
1804 1807 id : <id_given_in_input>
1805 1808 result : null
1806 1809 error : {
1807 1810 "Unable to pull changes from `<reponame>`"
1808 1811 }
1809 1812
1810 1813 """
1811 1814
1812 1815 repo = get_repo_or_error(repoid)
1813 1816 if not has_superadmin_permission(apiuser):
1814 1817 _perms = ('repository.admin',)
1815 1818 validate_repo_permissions(apiuser, repoid, repo, _perms)
1816 1819
1817 1820 try:
1818 1821 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1819 1822 return {
1820 1823 'msg': 'Pulled from `%s`' % repo.repo_name,
1821 1824 'repository': repo.repo_name
1822 1825 }
1823 1826 except Exception:
1824 1827 log.exception("Exception occurred while trying to "
1825 1828 "pull changes from remote location")
1826 1829 raise JSONRPCError(
1827 1830 'Unable to pull changes from `%s`' % repo.repo_name
1828 1831 )
1829 1832
1830 1833
1831 1834 @jsonrpc_method()
1832 1835 def strip(request, apiuser, repoid, revision, branch):
1833 1836 """
1834 1837 Strips the given revision from the specified repository.
1835 1838
1836 1839 * This will remove the revision and all of its decendants.
1837 1840
1838 1841 This command can only be run using an |authtoken| with admin rights to
1839 1842 the specified repository.
1840 1843
1841 1844 This command takes the following options:
1842 1845
1843 1846 :param apiuser: This is filled automatically from the |authtoken|.
1844 1847 :type apiuser: AuthUser
1845 1848 :param repoid: The repository name or repository ID.
1846 1849 :type repoid: str or int
1847 1850 :param revision: The revision you wish to strip.
1848 1851 :type revision: str
1849 1852 :param branch: The branch from which to strip the revision.
1850 1853 :type branch: str
1851 1854
1852 1855 Example output:
1853 1856
1854 1857 .. code-block:: bash
1855 1858
1856 1859 id : <id_given_in_input>
1857 1860 result : {
1858 1861 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1859 1862 "repository": "<repository name>"
1860 1863 }
1861 1864 error : null
1862 1865
1863 1866 Example error output:
1864 1867
1865 1868 .. code-block:: bash
1866 1869
1867 1870 id : <id_given_in_input>
1868 1871 result : null
1869 1872 error : {
1870 1873 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1871 1874 }
1872 1875
1873 1876 """
1874 1877
1875 1878 repo = get_repo_or_error(repoid)
1876 1879 if not has_superadmin_permission(apiuser):
1877 1880 _perms = ('repository.admin',)
1878 1881 validate_repo_permissions(apiuser, repoid, repo, _perms)
1879 1882
1880 1883 try:
1881 1884 ScmModel().strip(repo, revision, branch)
1885 audit_logger.store_api(
1886 'repo.commit.strip', action_data={'commit_id': revision},
1887 repo=repo,
1888 user=apiuser, commit=True)
1889
1882 1890 return {
1883 1891 'msg': 'Stripped commit %s from repo `%s`' % (
1884 1892 revision, repo.repo_name),
1885 1893 'repository': repo.repo_name
1886 1894 }
1887 1895 except Exception:
1888 1896 log.exception("Exception while trying to strip")
1889 1897 raise JSONRPCError(
1890 1898 'Unable to strip commit %s from repo `%s`' % (
1891 1899 revision, repo.repo_name)
1892 1900 )
1893 1901
1894 1902
1895 1903 @jsonrpc_method()
1896 1904 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1897 1905 """
1898 1906 Returns all settings for a repository. If key is given it only returns the
1899 1907 setting identified by the key or null.
1900 1908
1901 1909 :param apiuser: This is filled automatically from the |authtoken|.
1902 1910 :type apiuser: AuthUser
1903 1911 :param repoid: The repository name or repository id.
1904 1912 :type repoid: str or int
1905 1913 :param key: Key of the setting to return.
1906 1914 :type: key: Optional(str)
1907 1915
1908 1916 Example output:
1909 1917
1910 1918 .. code-block:: bash
1911 1919
1912 1920 {
1913 1921 "error": null,
1914 1922 "id": 237,
1915 1923 "result": {
1916 1924 "extensions_largefiles": true,
1917 1925 "extensions_evolve": true,
1918 1926 "hooks_changegroup_push_logger": true,
1919 1927 "hooks_changegroup_repo_size": false,
1920 1928 "hooks_outgoing_pull_logger": true,
1921 1929 "phases_publish": "True",
1922 1930 "rhodecode_hg_use_rebase_for_merging": true,
1923 1931 "rhodecode_pr_merge_enabled": true,
1924 1932 "rhodecode_use_outdated_comments": true
1925 1933 }
1926 1934 }
1927 1935 """
1928 1936
1929 1937 # Restrict access to this api method to admins only.
1930 1938 if not has_superadmin_permission(apiuser):
1931 1939 raise JSONRPCForbidden()
1932 1940
1933 1941 try:
1934 1942 repo = get_repo_or_error(repoid)
1935 1943 settings_model = VcsSettingsModel(repo=repo)
1936 1944 settings = settings_model.get_global_settings()
1937 1945 settings.update(settings_model.get_repo_settings())
1938 1946
1939 1947 # If only a single setting is requested fetch it from all settings.
1940 1948 key = Optional.extract(key)
1941 1949 if key is not None:
1942 1950 settings = settings.get(key, None)
1943 1951 except Exception:
1944 1952 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1945 1953 log.exception(msg)
1946 1954 raise JSONRPCError(msg)
1947 1955
1948 1956 return settings
1949 1957
1950 1958
1951 1959 @jsonrpc_method()
1952 1960 def set_repo_settings(request, apiuser, repoid, settings):
1953 1961 """
1954 1962 Update repository settings. Returns true on success.
1955 1963
1956 1964 :param apiuser: This is filled automatically from the |authtoken|.
1957 1965 :type apiuser: AuthUser
1958 1966 :param repoid: The repository name or repository id.
1959 1967 :type repoid: str or int
1960 1968 :param settings: The new settings for the repository.
1961 1969 :type: settings: dict
1962 1970
1963 1971 Example output:
1964 1972
1965 1973 .. code-block:: bash
1966 1974
1967 1975 {
1968 1976 "error": null,
1969 1977 "id": 237,
1970 1978 "result": true
1971 1979 }
1972 1980 """
1973 1981 # Restrict access to this api method to admins only.
1974 1982 if not has_superadmin_permission(apiuser):
1975 1983 raise JSONRPCForbidden()
1976 1984
1977 1985 if type(settings) is not dict:
1978 1986 raise JSONRPCError('Settings have to be a JSON Object.')
1979 1987
1980 1988 try:
1981 1989 settings_model = VcsSettingsModel(repo=repoid)
1982 1990
1983 1991 # Merge global, repo and incoming settings.
1984 1992 new_settings = settings_model.get_global_settings()
1985 1993 new_settings.update(settings_model.get_repo_settings())
1986 1994 new_settings.update(settings)
1987 1995
1988 1996 # Update the settings.
1989 1997 inherit_global_settings = new_settings.get(
1990 1998 'inherit_global_settings', False)
1991 1999 settings_model.create_or_update_repo_settings(
1992 2000 new_settings, inherit_global_settings=inherit_global_settings)
1993 2001 Session().commit()
1994 2002 except Exception:
1995 2003 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1996 2004 log.exception(msg)
1997 2005 raise JSONRPCError(msg)
1998 2006
1999 2007 # Indicate success.
2000 2008 return True
2001 2009
2002 2010
2003 2011 @jsonrpc_method()
2004 2012 def maintenance(request, apiuser, repoid):
2005 2013 """
2006 2014 Triggers a maintenance on the given repository.
2007 2015
2008 2016 This command can only be run using an |authtoken| with admin
2009 2017 rights to the specified repository. For more information,
2010 2018 see :ref:`config-token-ref`.
2011 2019
2012 2020 This command takes the following options:
2013 2021
2014 2022 :param apiuser: This is filled automatically from the |authtoken|.
2015 2023 :type apiuser: AuthUser
2016 2024 :param repoid: The repository name or repository ID.
2017 2025 :type repoid: str or int
2018 2026
2019 2027 Example output:
2020 2028
2021 2029 .. code-block:: bash
2022 2030
2023 2031 id : <id_given_in_input>
2024 2032 result : {
2025 2033 "msg": "executed maintenance command",
2026 2034 "executed_actions": [
2027 2035 <action_message>, <action_message2>...
2028 2036 ],
2029 2037 "repository": "<repository name>"
2030 2038 }
2031 2039 error : null
2032 2040
2033 2041 Example error output:
2034 2042
2035 2043 .. code-block:: bash
2036 2044
2037 2045 id : <id_given_in_input>
2038 2046 result : null
2039 2047 error : {
2040 2048 "Unable to execute maintenance on `<reponame>`"
2041 2049 }
2042 2050
2043 2051 """
2044 2052
2045 2053 repo = get_repo_or_error(repoid)
2046 2054 if not has_superadmin_permission(apiuser):
2047 2055 _perms = ('repository.admin',)
2048 2056 validate_repo_permissions(apiuser, repoid, repo, _perms)
2049 2057
2050 2058 try:
2051 2059 maintenance = repo_maintenance.RepoMaintenance()
2052 2060 executed_actions = maintenance.execute(repo)
2053 2061
2054 2062 return {
2055 2063 'msg': 'executed maintenance command',
2056 2064 'executed_actions': executed_actions,
2057 2065 'repository': repo.repo_name
2058 2066 }
2059 2067 except Exception:
2060 2068 log.exception("Exception occurred while trying to run maintenance")
2061 2069 raise JSONRPCError(
2062 2070 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,702 +1,719 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
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 from rhodecode.lib import audit_logger
30 31 from rhodecode.lib.auth import (
31 32 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
32 33 from rhodecode.model.db import Session
33 34 from rhodecode.model.repo_group import RepoGroupModel
34 35 from rhodecode.model.scm import RepoGroupList
35 36 from rhodecode.model import validation_schema
36 37 from rhodecode.model.validation_schema.schemas import repo_group_schema
37 38
38 39
39 40 log = logging.getLogger(__name__)
40 41
41 42
42 43 @jsonrpc_method()
43 44 def get_repo_group(request, apiuser, repogroupid):
44 45 """
45 46 Return the specified |repo| group, along with permissions,
46 47 and repositories inside the group
47 48
48 49 :param apiuser: This is filled automatically from the |authtoken|.
49 50 :type apiuser: AuthUser
50 51 :param repogroupid: Specify the name of ID of the repository group.
51 52 :type repogroupid: str or int
52 53
53 54
54 55 Example output:
55 56
56 57 .. code-block:: bash
57 58
58 59 {
59 60 "error": null,
60 61 "id": repo-group-id,
61 62 "result": {
62 63 "group_description": "repo group description",
63 64 "group_id": 14,
64 65 "group_name": "group name",
65 66 "members": [
66 67 {
67 68 "name": "super-admin-username",
68 69 "origin": "super-admin",
69 70 "permission": "group.admin",
70 71 "type": "user"
71 72 },
72 73 {
73 74 "name": "owner-name",
74 75 "origin": "owner",
75 76 "permission": "group.admin",
76 77 "type": "user"
77 78 },
78 79 {
79 80 "name": "user-group-name",
80 81 "origin": "permission",
81 82 "permission": "group.write",
82 83 "type": "user_group"
83 84 }
84 85 ],
85 86 "owner": "owner-name",
86 87 "parent_group": null,
87 88 "repositories": [ repo-list ]
88 89 }
89 90 }
90 91 """
91 92
92 93 repo_group = get_repo_group_or_error(repogroupid)
93 94 if not has_superadmin_permission(apiuser):
94 95 # check if we have at least read permission for this repo group !
95 96 _perms = ('group.admin', 'group.write', 'group.read',)
96 97 if not HasRepoGroupPermissionAnyApi(*_perms)(
97 98 user=apiuser, group_name=repo_group.group_name):
98 99 raise JSONRPCError(
99 100 'repository group `%s` does not exist' % (repogroupid,))
100 101
101 102 permissions = []
102 103 for _user in repo_group.permissions():
103 104 user_data = {
104 105 'name': _user.username,
105 106 'permission': _user.permission,
106 107 'origin': get_origin(_user),
107 108 'type': "user",
108 109 }
109 110 permissions.append(user_data)
110 111
111 112 for _user_group in repo_group.permission_user_groups():
112 113 user_group_data = {
113 114 'name': _user_group.users_group_name,
114 115 'permission': _user_group.permission,
115 116 'origin': get_origin(_user_group),
116 117 'type': "user_group",
117 118 }
118 119 permissions.append(user_group_data)
119 120
120 121 data = repo_group.get_api_data()
121 122 data["members"] = permissions # TODO: this should be named permissions
122 123 return data
123 124
124 125
125 126 @jsonrpc_method()
126 127 def get_repo_groups(request, apiuser):
127 128 """
128 129 Returns all repository groups.
129 130
130 131 :param apiuser: This is filled automatically from the |authtoken|.
131 132 :type apiuser: AuthUser
132 133 """
133 134
134 135 result = []
135 136 _perms = ('group.read', 'group.write', 'group.admin',)
136 137 extras = {'user': apiuser}
137 138 for repo_group in RepoGroupList(RepoGroupModel().get_all(),
138 139 perm_set=_perms, extra_kwargs=extras):
139 140 result.append(repo_group.get_api_data())
140 141 return result
141 142
142 143
143 144 @jsonrpc_method()
144 145 def create_repo_group(
145 146 request, apiuser, group_name,
146 147 owner=Optional(OAttr('apiuser')),
147 148 description=Optional(''),
148 149 copy_permissions=Optional(False)):
149 150 """
150 151 Creates a repository group.
151 152
152 153 * If the repository group name contains "/", repository group will be
153 154 created inside a repository group or nested repository groups
154 155
155 156 For example "foo/bar/group1" will create repository group called "group1"
156 157 inside group "foo/bar". You have to have permissions to access and
157 158 write to the last repository group ("bar" in this example)
158 159
159 160 This command can only be run using an |authtoken| with at least
160 161 permissions to create repository groups, or admin permissions to
161 162 parent repository groups.
162 163
163 164 :param apiuser: This is filled automatically from the |authtoken|.
164 165 :type apiuser: AuthUser
165 166 :param group_name: Set the repository group name.
166 167 :type group_name: str
167 168 :param description: Set the |repo| group description.
168 169 :type description: str
169 170 :param owner: Set the |repo| group owner.
170 171 :type owner: str
171 172 :param copy_permissions:
172 173 :type copy_permissions:
173 174
174 175 Example output:
175 176
176 177 .. code-block:: bash
177 178
178 179 id : <id_given_in_input>
179 180 result : {
180 181 "msg": "Created new repo group `<repo_group_name>`"
181 182 "repo_group": <repogroup_object>
182 183 }
183 184 error : null
184 185
185 186
186 187 Example error output:
187 188
188 189 .. code-block:: bash
189 190
190 191 id : <id_given_in_input>
191 192 result : null
192 193 error : {
193 194 failed to create repo group `<repogroupid>`
194 195 }
195 196
196 197 """
197 198
198 199 owner = validate_set_owner_permissions(apiuser, owner)
199 200
200 201 description = Optional.extract(description)
201 202 copy_permissions = Optional.extract(copy_permissions)
202 203
203 204 schema = repo_group_schema.RepoGroupSchema().bind(
204 205 # user caller
205 206 user=apiuser)
206 207
207 208 try:
208 209 schema_data = schema.deserialize(dict(
209 210 repo_group_name=group_name,
210 211 repo_group_owner=owner.username,
211 212 repo_group_description=description,
212 213 repo_group_copy_permissions=copy_permissions,
213 214 ))
214 215 except validation_schema.Invalid as err:
215 216 raise JSONRPCValidationError(colander_exc=err)
216 217
217 218 validated_group_name = schema_data['repo_group_name']
218 219
219 220 try:
220 221 repo_group = RepoGroupModel().create(
221 222 owner=owner,
222 223 group_name=validated_group_name,
223 224 group_description=schema_data['repo_group_name'],
224 225 copy_permissions=schema_data['repo_group_copy_permissions'])
226 Session().flush()
227
228 repo_group_data = repo_group.get_api_data()
229 audit_logger.store_api(
230 'repo_group.create', action_data={'data': repo_group_data},
231 user=apiuser)
232
225 233 Session().commit()
226 234 return {
227 235 'msg': 'Created new repo group `%s`' % validated_group_name,
228 236 'repo_group': repo_group.get_api_data()
229 237 }
230 238 except Exception:
231 239 log.exception("Exception occurred while trying create repo group")
232 240 raise JSONRPCError(
233 241 'failed to create repo group `%s`' % (validated_group_name,))
234 242
235 243
236 244 @jsonrpc_method()
237 245 def update_repo_group(
238 246 request, apiuser, repogroupid, group_name=Optional(''),
239 247 description=Optional(''), owner=Optional(OAttr('apiuser')),
240 248 enable_locking=Optional(False)):
241 249 """
242 250 Updates repository group with the details given.
243 251
244 252 This command can only be run using an |authtoken| with admin
245 253 permissions.
246 254
247 255 * If the group_name name contains "/", repository group will be updated
248 256 accordingly with a repository group or nested repository groups
249 257
250 258 For example repogroupid=group-test group_name="foo/bar/group-test"
251 259 will update repository group called "group-test" and place it
252 260 inside group "foo/bar".
253 261 You have to have permissions to access and write to the last repository
254 262 group ("bar" in this example)
255 263
256 264 :param apiuser: This is filled automatically from the |authtoken|.
257 265 :type apiuser: AuthUser
258 266 :param repogroupid: Set the ID of repository group.
259 267 :type repogroupid: str or int
260 268 :param group_name: Set the name of the |repo| group.
261 269 :type group_name: str
262 270 :param description: Set a description for the group.
263 271 :type description: str
264 272 :param owner: Set the |repo| group owner.
265 273 :type owner: str
266 274 :param enable_locking: Enable |repo| locking. The default is false.
267 275 :type enable_locking: bool
268 276 """
269 277
270 278 repo_group = get_repo_group_or_error(repogroupid)
271 279
272 280 if not has_superadmin_permission(apiuser):
273 281 validate_repo_group_permissions(
274 282 apiuser, repogroupid, repo_group, ('group.admin',))
275 283
276 284 updates = dict(
277 285 group_name=group_name
278 286 if not isinstance(group_name, Optional) else repo_group.group_name,
279 287
280 288 group_description=description
281 289 if not isinstance(description, Optional) else repo_group.group_description,
282 290
283 291 user=owner
284 292 if not isinstance(owner, Optional) else repo_group.user.username,
285 293
286 294 enable_locking=enable_locking
287 295 if not isinstance(enable_locking, Optional) else repo_group.enable_locking
288 296 )
289 297
290 298 schema = repo_group_schema.RepoGroupSchema().bind(
291 299 # user caller
292 300 user=apiuser,
293 301 old_values=repo_group.get_api_data())
294 302
295 303 try:
296 304 schema_data = schema.deserialize(dict(
297 305 repo_group_name=updates['group_name'],
298 306 repo_group_owner=updates['user'],
299 307 repo_group_description=updates['group_description'],
300 308 repo_group_enable_locking=updates['enable_locking'],
301 309 ))
302 310 except validation_schema.Invalid as err:
303 311 raise JSONRPCValidationError(colander_exc=err)
304 312
305 313 validated_updates = dict(
306 314 group_name=schema_data['repo_group']['repo_group_name_without_group'],
307 315 group_parent_id=schema_data['repo_group']['repo_group_id'],
308 316 user=schema_data['repo_group_owner'],
309 317 group_description=schema_data['repo_group_description'],
310 318 enable_locking=schema_data['repo_group_enable_locking'],
311 319 )
312 320
321 old_data = repo_group.get_api_data()
313 322 try:
314 323 RepoGroupModel().update(repo_group, validated_updates)
324 audit_logger.store_api(
325 'repo_group.edit', action_data={'old_data': old_data},
326 user=apiuser)
327
315 328 Session().commit()
316 329 return {
317 330 'msg': 'updated repository group ID:%s %s' % (
318 331 repo_group.group_id, repo_group.group_name),
319 332 'repo_group': repo_group.get_api_data()
320 333 }
321 334 except Exception:
322 335 log.exception(
323 336 u"Exception occurred while trying update repo group %s",
324 337 repogroupid)
325 338 raise JSONRPCError('failed to update repository group `%s`'
326 339 % (repogroupid,))
327 340
328 341
329 342 @jsonrpc_method()
330 343 def delete_repo_group(request, apiuser, repogroupid):
331 344 """
332 345 Deletes a |repo| group.
333 346
334 347 :param apiuser: This is filled automatically from the |authtoken|.
335 348 :type apiuser: AuthUser
336 349 :param repogroupid: Set the name or ID of repository group to be
337 350 deleted.
338 351 :type repogroupid: str or int
339 352
340 353 Example output:
341 354
342 355 .. code-block:: bash
343 356
344 357 id : <id_given_in_input>
345 358 result : {
346 359 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>'
347 360 'repo_group': null
348 361 }
349 362 error : null
350 363
351 364 Example error output:
352 365
353 366 .. code-block:: bash
354 367
355 368 id : <id_given_in_input>
356 369 result : null
357 370 error : {
358 371 "failed to delete repo group ID:<repogroupid> <repogroupname>"
359 372 }
360 373
361 374 """
362 375
363 376 repo_group = get_repo_group_or_error(repogroupid)
364 377 if not has_superadmin_permission(apiuser):
365 378 validate_repo_group_permissions(
366 379 apiuser, repogroupid, repo_group, ('group.admin',))
367 380
381 old_data = repo_group.get_api_data()
368 382 try:
369 383 RepoGroupModel().delete(repo_group)
384 audit_logger.store_api(
385 'repo_group.delete', action_data={'old_data': old_data},
386 user=apiuser)
370 387 Session().commit()
371 388 return {
372 389 'msg': 'deleted repo group ID:%s %s' %
373 390 (repo_group.group_id, repo_group.group_name),
374 391 'repo_group': None
375 392 }
376 393 except Exception:
377 394 log.exception("Exception occurred while trying to delete repo group")
378 395 raise JSONRPCError('failed to delete repo group ID:%s %s' %
379 396 (repo_group.group_id, repo_group.group_name))
380 397
381 398
382 399 @jsonrpc_method()
383 400 def grant_user_permission_to_repo_group(
384 401 request, apiuser, repogroupid, userid, perm,
385 402 apply_to_children=Optional('none')):
386 403 """
387 404 Grant permission for a user on the given repository group, or update
388 405 existing permissions if found.
389 406
390 407 This command can only be run using an |authtoken| with admin
391 408 permissions.
392 409
393 410 :param apiuser: This is filled automatically from the |authtoken|.
394 411 :type apiuser: AuthUser
395 412 :param repogroupid: Set the name or ID of repository group.
396 413 :type repogroupid: str or int
397 414 :param userid: Set the user name.
398 415 :type userid: str
399 416 :param perm: (group.(none|read|write|admin))
400 417 :type perm: str
401 418 :param apply_to_children: 'none', 'repos', 'groups', 'all'
402 419 :type apply_to_children: str
403 420
404 421 Example output:
405 422
406 423 .. code-block:: bash
407 424
408 425 id : <id_given_in_input>
409 426 result: {
410 427 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
411 428 "success": true
412 429 }
413 430 error: null
414 431
415 432 Example error output:
416 433
417 434 .. code-block:: bash
418 435
419 436 id : <id_given_in_input>
420 437 result : null
421 438 error : {
422 439 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
423 440 }
424 441
425 442 """
426 443
427 444 repo_group = get_repo_group_or_error(repogroupid)
428 445
429 446 if not has_superadmin_permission(apiuser):
430 447 validate_repo_group_permissions(
431 448 apiuser, repogroupid, repo_group, ('group.admin',))
432 449
433 450 user = get_user_or_error(userid)
434 451 perm = get_perm_or_error(perm, prefix='group.')
435 452 apply_to_children = Optional.extract(apply_to_children)
436 453
437 454 perm_additions = [[user.user_id, perm, "user"]]
438 455 try:
439 456 RepoGroupModel().update_permissions(repo_group=repo_group,
440 457 perm_additions=perm_additions,
441 458 recursive=apply_to_children,
442 459 cur_user=apiuser)
443 460 Session().commit()
444 461 return {
445 462 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
446 463 '`%s` in repo group: `%s`' % (
447 464 perm.permission_name, apply_to_children, user.username,
448 465 repo_group.name
449 466 ),
450 467 'success': True
451 468 }
452 469 except Exception:
453 470 log.exception("Exception occurred while trying to grant "
454 471 "user permissions to repo group")
455 472 raise JSONRPCError(
456 473 'failed to edit permission for user: '
457 474 '`%s` in repo group: `%s`' % (userid, repo_group.name))
458 475
459 476
460 477 @jsonrpc_method()
461 478 def revoke_user_permission_from_repo_group(
462 479 request, apiuser, repogroupid, userid,
463 480 apply_to_children=Optional('none')):
464 481 """
465 482 Revoke permission for a user in a given repository group.
466 483
467 484 This command can only be run using an |authtoken| with admin
468 485 permissions on the |repo| group.
469 486
470 487 :param apiuser: This is filled automatically from the |authtoken|.
471 488 :type apiuser: AuthUser
472 489 :param repogroupid: Set the name or ID of the repository group.
473 490 :type repogroupid: str or int
474 491 :param userid: Set the user name to revoke.
475 492 :type userid: str
476 493 :param apply_to_children: 'none', 'repos', 'groups', 'all'
477 494 :type apply_to_children: str
478 495
479 496 Example output:
480 497
481 498 .. code-block:: bash
482 499
483 500 id : <id_given_in_input>
484 501 result: {
485 502 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
486 503 "success": true
487 504 }
488 505 error: null
489 506
490 507 Example error output:
491 508
492 509 .. code-block:: bash
493 510
494 511 id : <id_given_in_input>
495 512 result : null
496 513 error : {
497 514 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
498 515 }
499 516
500 517 """
501 518
502 519 repo_group = get_repo_group_or_error(repogroupid)
503 520
504 521 if not has_superadmin_permission(apiuser):
505 522 validate_repo_group_permissions(
506 523 apiuser, repogroupid, repo_group, ('group.admin',))
507 524
508 525 user = get_user_or_error(userid)
509 526 apply_to_children = Optional.extract(apply_to_children)
510 527
511 528 perm_deletions = [[user.user_id, None, "user"]]
512 529 try:
513 530 RepoGroupModel().update_permissions(repo_group=repo_group,
514 531 perm_deletions=perm_deletions,
515 532 recursive=apply_to_children,
516 533 cur_user=apiuser)
517 534 Session().commit()
518 535 return {
519 536 'msg': 'Revoked perm (recursive:%s) for user: '
520 537 '`%s` in repo group: `%s`' % (
521 538 apply_to_children, user.username, repo_group.name
522 539 ),
523 540 'success': True
524 541 }
525 542 except Exception:
526 543 log.exception("Exception occurred while trying revoke user "
527 544 "permission from repo group")
528 545 raise JSONRPCError(
529 546 'failed to edit permission for user: '
530 547 '`%s` in repo group: `%s`' % (userid, repo_group.name))
531 548
532 549
533 550 @jsonrpc_method()
534 551 def grant_user_group_permission_to_repo_group(
535 552 request, apiuser, repogroupid, usergroupid, perm,
536 553 apply_to_children=Optional('none'), ):
537 554 """
538 555 Grant permission for a user group on given repository group, or update
539 556 existing permissions if found.
540 557
541 558 This command can only be run using an |authtoken| with admin
542 559 permissions on the |repo| group.
543 560
544 561 :param apiuser: This is filled automatically from the |authtoken|.
545 562 :type apiuser: AuthUser
546 563 :param repogroupid: Set the name or id of repository group
547 564 :type repogroupid: str or int
548 565 :param usergroupid: id of usergroup
549 566 :type usergroupid: str or int
550 567 :param perm: (group.(none|read|write|admin))
551 568 :type perm: str
552 569 :param apply_to_children: 'none', 'repos', 'groups', 'all'
553 570 :type apply_to_children: str
554 571
555 572 Example output:
556 573
557 574 .. code-block:: bash
558 575
559 576 id : <id_given_in_input>
560 577 result : {
561 578 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
562 579 "success": true
563 580
564 581 }
565 582 error : null
566 583
567 584 Example error output:
568 585
569 586 .. code-block:: bash
570 587
571 588 id : <id_given_in_input>
572 589 result : null
573 590 error : {
574 591 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
575 592 }
576 593
577 594 """
578 595
579 596 repo_group = get_repo_group_or_error(repogroupid)
580 597 perm = get_perm_or_error(perm, prefix='group.')
581 598 user_group = get_user_group_or_error(usergroupid)
582 599 if not has_superadmin_permission(apiuser):
583 600 validate_repo_group_permissions(
584 601 apiuser, repogroupid, repo_group, ('group.admin',))
585 602
586 603 # check if we have at least read permission for this user group !
587 604 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
588 605 if not HasUserGroupPermissionAnyApi(*_perms)(
589 606 user=apiuser, user_group_name=user_group.users_group_name):
590 607 raise JSONRPCError(
591 608 'user group `%s` does not exist' % (usergroupid,))
592 609
593 610 apply_to_children = Optional.extract(apply_to_children)
594 611
595 612 perm_additions = [[user_group.users_group_id, perm, "user_group"]]
596 613 try:
597 614 RepoGroupModel().update_permissions(repo_group=repo_group,
598 615 perm_additions=perm_additions,
599 616 recursive=apply_to_children,
600 617 cur_user=apiuser)
601 618 Session().commit()
602 619 return {
603 620 'msg': 'Granted perm: `%s` (recursive:%s) '
604 621 'for user group: `%s` in repo group: `%s`' % (
605 622 perm.permission_name, apply_to_children,
606 623 user_group.users_group_name, repo_group.name
607 624 ),
608 625 'success': True
609 626 }
610 627 except Exception:
611 628 log.exception("Exception occurred while trying to grant user "
612 629 "group permissions to repo group")
613 630 raise JSONRPCError(
614 631 'failed to edit permission for user group: `%s` in '
615 632 'repo group: `%s`' % (
616 633 usergroupid, repo_group.name
617 634 )
618 635 )
619 636
620 637
621 638 @jsonrpc_method()
622 639 def revoke_user_group_permission_from_repo_group(
623 640 request, apiuser, repogroupid, usergroupid,
624 641 apply_to_children=Optional('none')):
625 642 """
626 643 Revoke permission for user group on given repository.
627 644
628 645 This command can only be run using an |authtoken| with admin
629 646 permissions on the |repo| group.
630 647
631 648 :param apiuser: This is filled automatically from the |authtoken|.
632 649 :type apiuser: AuthUser
633 650 :param repogroupid: name or id of repository group
634 651 :type repogroupid: str or int
635 652 :param usergroupid:
636 653 :param apply_to_children: 'none', 'repos', 'groups', 'all'
637 654 :type apply_to_children: str
638 655
639 656 Example output:
640 657
641 658 .. code-block:: bash
642 659
643 660 id : <id_given_in_input>
644 661 result: {
645 662 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
646 663 "success": true
647 664 }
648 665 error: null
649 666
650 667 Example error output:
651 668
652 669 .. code-block:: bash
653 670
654 671 id : <id_given_in_input>
655 672 result : null
656 673 error : {
657 674 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
658 675 }
659 676
660 677
661 678 """
662 679
663 680 repo_group = get_repo_group_or_error(repogroupid)
664 681 user_group = get_user_group_or_error(usergroupid)
665 682 if not has_superadmin_permission(apiuser):
666 683 validate_repo_group_permissions(
667 684 apiuser, repogroupid, repo_group, ('group.admin',))
668 685
669 686 # check if we have at least read permission for this user group !
670 687 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
671 688 if not HasUserGroupPermissionAnyApi(*_perms)(
672 689 user=apiuser, user_group_name=user_group.users_group_name):
673 690 raise JSONRPCError(
674 691 'user group `%s` does not exist' % (usergroupid,))
675 692
676 693 apply_to_children = Optional.extract(apply_to_children)
677 694
678 695 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
679 696 try:
680 697 RepoGroupModel().update_permissions(repo_group=repo_group,
681 698 perm_deletions=perm_deletions,
682 699 recursive=apply_to_children,
683 700 cur_user=apiuser)
684 701 Session().commit()
685 702 return {
686 703 'msg': 'Revoked perm (recursive:%s) for user group: '
687 704 '`%s` in repo group: `%s`' % (
688 705 apply_to_children, user_group.users_group_name,
689 706 repo_group.name
690 707 ),
691 708 'success': True
692 709 }
693 710 except Exception:
694 711 log.exception("Exception occurred while trying revoke user group "
695 712 "permissions from repo group")
696 713 raise JSONRPCError(
697 714 'failed to edit permission for user group: '
698 715 '`%s` in repo group: `%s`' % (
699 716 user_group.users_group_name, repo_group.name
700 717 )
701 718 )
702 719
@@ -1,515 +1,529 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
24 24 from rhodecode.api.utils import (
25 25 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
26 from rhodecode.lib import audit_logger
26 27 from rhodecode.lib.auth import AuthUser, PasswordGenerator
27 28 from rhodecode.lib.exceptions import DefaultUserException
28 29 from rhodecode.lib.utils2 import safe_int, str2bool
29 30 from rhodecode.model.db import Session, User, Repository
30 31 from rhodecode.model.user import UserModel
31 32
32 33 log = logging.getLogger(__name__)
33 34
34 35
35 36 @jsonrpc_method()
36 37 def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))):
37 38 """
38 39 Returns the information associated with a username or userid.
39 40
40 41 * If the ``userid`` is not set, this command returns the information
41 42 for the ``userid`` calling the method.
42 43
43 44 .. note::
44 45
45 46 Normal users may only run this command against their ``userid``. For
46 47 full privileges you must run this command using an |authtoken| with
47 48 admin rights.
48 49
49 50 :param apiuser: This is filled automatically from the |authtoken|.
50 51 :type apiuser: AuthUser
51 52 :param userid: Sets the userid for which data will be returned.
52 53 :type userid: Optional(str or int)
53 54
54 55 Example output:
55 56
56 57 .. code-block:: bash
57 58
58 59 {
59 60 "error": null,
60 61 "id": <id>,
61 62 "result": {
62 63 "active": true,
63 64 "admin": false,
64 65 "api_keys": [ list of keys ],
65 66 "auth_tokens": [ list of tokens with details ],
66 67 "email": "user@example.com",
67 68 "emails": [
68 69 "user@example.com"
69 70 ],
70 71 "extern_name": "rhodecode",
71 72 "extern_type": "rhodecode",
72 73 "firstname": "username",
73 74 "ip_addresses": [],
74 75 "language": null,
75 76 "last_login": "Timestamp",
76 77 "last_activity": "Timestamp",
77 78 "lastname": "surnae",
78 79 "permissions": {
79 80 "global": [
80 81 "hg.inherit_default_perms.true",
81 82 "usergroup.read",
82 83 "hg.repogroup.create.false",
83 84 "hg.create.none",
84 85 "hg.password_reset.enabled",
85 86 "hg.extern_activate.manual",
86 87 "hg.create.write_on_repogroup.false",
87 88 "hg.usergroup.create.false",
88 89 "group.none",
89 90 "repository.none",
90 91 "hg.register.none",
91 92 "hg.fork.repository"
92 93 ],
93 94 "repositories": { "username/example": "repository.write"},
94 95 "repositories_groups": { "user-group/repo": "group.none" },
95 96 "user_groups": { "user_group_name": "usergroup.read" }
96 97 },
97 98 "user_id": 32,
98 99 "username": "username"
99 100 }
100 101 }
101 102 """
102 103
103 104 if not has_superadmin_permission(apiuser):
104 105 # make sure normal user does not pass someone else userid,
105 106 # he is not allowed to do that
106 107 if not isinstance(userid, Optional) and userid != apiuser.user_id:
107 108 raise JSONRPCError('userid is not the same as your user')
108 109
109 110 userid = Optional.extract(userid, evaluate_locals=locals())
110 111 userid = getattr(userid, 'user_id', userid)
111 112
112 113 user = get_user_or_error(userid)
113 114 data = user.get_api_data(include_secrets=True)
114 115 data['permissions'] = AuthUser(user_id=user.user_id).permissions
115 116 return data
116 117
117 118
118 119 @jsonrpc_method()
119 120 def get_users(request, apiuser):
120 121 """
121 122 Lists all users in the |RCE| user database.
122 123
123 124 This command can only be run using an |authtoken| with admin rights to
124 125 the specified repository.
125 126
126 127 This command takes the following options:
127 128
128 129 :param apiuser: This is filled automatically from the |authtoken|.
129 130 :type apiuser: AuthUser
130 131
131 132 Example output:
132 133
133 134 .. code-block:: bash
134 135
135 136 id : <id_given_in_input>
136 137 result: [<user_object>, ...]
137 138 error: null
138 139 """
139 140
140 141 if not has_superadmin_permission(apiuser):
141 142 raise JSONRPCForbidden()
142 143
143 144 result = []
144 145 users_list = User.query().order_by(User.username) \
145 146 .filter(User.username != User.DEFAULT_USER) \
146 147 .all()
147 148 for user in users_list:
148 149 result.append(user.get_api_data(include_secrets=True))
149 150 return result
150 151
151 152
152 153 @jsonrpc_method()
153 154 def create_user(request, apiuser, username, email, password=Optional(''),
154 155 firstname=Optional(''), lastname=Optional(''),
155 156 active=Optional(True), admin=Optional(False),
156 157 extern_name=Optional('rhodecode'),
157 158 extern_type=Optional('rhodecode'),
158 159 force_password_change=Optional(False),
159 160 create_personal_repo_group=Optional(None)):
160 161 """
161 162 Creates a new user and returns the new user object.
162 163
163 164 This command can only be run using an |authtoken| with admin rights to
164 165 the specified repository.
165 166
166 167 This command takes the following options:
167 168
168 169 :param apiuser: This is filled automatically from the |authtoken|.
169 170 :type apiuser: AuthUser
170 171 :param username: Set the new username.
171 172 :type username: str or int
172 173 :param email: Set the user email address.
173 174 :type email: str
174 175 :param password: Set the new user password.
175 176 :type password: Optional(str)
176 177 :param firstname: Set the new user firstname.
177 178 :type firstname: Optional(str)
178 179 :param lastname: Set the new user surname.
179 180 :type lastname: Optional(str)
180 181 :param active: Set the user as active.
181 182 :type active: Optional(``True`` | ``False``)
182 183 :param admin: Give the new user admin rights.
183 184 :type admin: Optional(``True`` | ``False``)
184 185 :param extern_name: Set the authentication plugin name.
185 186 Using LDAP this is filled with LDAP UID.
186 187 :type extern_name: Optional(str)
187 188 :param extern_type: Set the new user authentication plugin.
188 189 :type extern_type: Optional(str)
189 190 :param force_password_change: Force the new user to change password
190 191 on next login.
191 192 :type force_password_change: Optional(``True`` | ``False``)
192 193 :param create_personal_repo_group: Create personal repo group for this user
193 194 :type create_personal_repo_group: Optional(``True`` | ``False``)
194 195
195 196 Example output:
196 197
197 198 .. code-block:: bash
198 199
199 200 id : <id_given_in_input>
200 201 result: {
201 202 "msg" : "created new user `<username>`",
202 203 "user": <user_obj>
203 204 }
204 205 error: null
205 206
206 207 Example error output:
207 208
208 209 .. code-block:: bash
209 210
210 211 id : <id_given_in_input>
211 212 result : null
212 213 error : {
213 214 "user `<username>` already exist"
214 215 or
215 216 "email `<email>` already exist"
216 217 or
217 218 "failed to create user `<username>`"
218 219 }
219 220
220 221 """
221 222 if not has_superadmin_permission(apiuser):
222 223 raise JSONRPCForbidden()
223 224
224 225 if UserModel().get_by_username(username):
225 226 raise JSONRPCError("user `%s` already exist" % (username,))
226 227
227 228 if UserModel().get_by_email(email, case_insensitive=True):
228 229 raise JSONRPCError("email `%s` already exist" % (email,))
229 230
230 231 # generate random password if we actually given the
231 232 # extern_name and it's not rhodecode
232 233 if (not isinstance(extern_name, Optional) and
233 234 Optional.extract(extern_name) != 'rhodecode'):
234 235 # generate temporary password if user is external
235 236 password = PasswordGenerator().gen_password(length=16)
236 237 create_repo_group = Optional.extract(create_personal_repo_group)
237 238 if isinstance(create_repo_group, basestring):
238 239 create_repo_group = str2bool(create_repo_group)
239 240
240 241 try:
241 242 user = UserModel().create_or_update(
242 243 username=Optional.extract(username),
243 244 password=Optional.extract(password),
244 245 email=Optional.extract(email),
245 246 firstname=Optional.extract(firstname),
246 247 lastname=Optional.extract(lastname),
247 248 active=Optional.extract(active),
248 249 admin=Optional.extract(admin),
249 250 extern_type=Optional.extract(extern_type),
250 251 extern_name=Optional.extract(extern_name),
251 252 force_password_change=Optional.extract(force_password_change),
252 253 create_repo_group=create_repo_group
253 254 )
255 Session().flush()
256 creation_data = user.get_api_data()
257 audit_logger.store_api(
258 'user.create', action_data={'data': creation_data},
259 user=apiuser)
260
254 261 Session().commit()
255 262 return {
256 263 'msg': 'created new user `%s`' % username,
257 264 'user': user.get_api_data(include_secrets=True)
258 265 }
259 266 except Exception:
260 267 log.exception('Error occurred during creation of user')
261 268 raise JSONRPCError('failed to create user `%s`' % (username,))
262 269
263 270
264 271 @jsonrpc_method()
265 272 def update_user(request, apiuser, userid, username=Optional(None),
266 273 email=Optional(None), password=Optional(None),
267 274 firstname=Optional(None), lastname=Optional(None),
268 275 active=Optional(None), admin=Optional(None),
269 276 extern_type=Optional(None), extern_name=Optional(None), ):
270 277 """
271 278 Updates the details for the specified user, if that user exists.
272 279
273 280 This command can only be run using an |authtoken| with admin rights to
274 281 the specified repository.
275 282
276 283 This command takes the following options:
277 284
278 285 :param apiuser: This is filled automatically from |authtoken|.
279 286 :type apiuser: AuthUser
280 287 :param userid: Set the ``userid`` to update.
281 288 :type userid: str or int
282 289 :param username: Set the new username.
283 290 :type username: str or int
284 291 :param email: Set the new email.
285 292 :type email: str
286 293 :param password: Set the new password.
287 294 :type password: Optional(str)
288 295 :param firstname: Set the new first name.
289 296 :type firstname: Optional(str)
290 297 :param lastname: Set the new surname.
291 298 :type lastname: Optional(str)
292 299 :param active: Set the new user as active.
293 300 :type active: Optional(``True`` | ``False``)
294 301 :param admin: Give the user admin rights.
295 302 :type admin: Optional(``True`` | ``False``)
296 303 :param extern_name: Set the authentication plugin user name.
297 304 Using LDAP this is filled with LDAP UID.
298 305 :type extern_name: Optional(str)
299 306 :param extern_type: Set the authentication plugin type.
300 307 :type extern_type: Optional(str)
301 308
302 309
303 310 Example output:
304 311
305 312 .. code-block:: bash
306 313
307 314 id : <id_given_in_input>
308 315 result: {
309 316 "msg" : "updated user ID:<userid> <username>",
310 317 "user": <user_object>,
311 318 }
312 319 error: null
313 320
314 321 Example error output:
315 322
316 323 .. code-block:: bash
317 324
318 325 id : <id_given_in_input>
319 326 result : null
320 327 error : {
321 328 "failed to update user `<username>`"
322 329 }
323 330
324 331 """
325 332 if not has_superadmin_permission(apiuser):
326 333 raise JSONRPCForbidden()
327 334
328 335 user = get_user_or_error(userid)
329
336 old_data = user.get_api_data()
330 337 # only non optional arguments will be stored in updates
331 338 updates = {}
332 339
333 340 try:
334 341
335 342 store_update(updates, username, 'username')
336 343 store_update(updates, password, 'password')
337 344 store_update(updates, email, 'email')
338 345 store_update(updates, firstname, 'name')
339 346 store_update(updates, lastname, 'lastname')
340 347 store_update(updates, active, 'active')
341 348 store_update(updates, admin, 'admin')
342 349 store_update(updates, extern_name, 'extern_name')
343 350 store_update(updates, extern_type, 'extern_type')
344 351
345 352 user = UserModel().update_user(user, **updates)
353 audit_logger.store_api(
354 'user.edit', action_data={'old_data': old_data},
355 user=apiuser)
346 356 Session().commit()
347 357 return {
348 358 'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
349 359 'user': user.get_api_data(include_secrets=True)
350 360 }
351 361 except DefaultUserException:
352 362 log.exception("Default user edit exception")
353 363 raise JSONRPCError('editing default user is forbidden')
354 364 except Exception:
355 365 log.exception("Error occurred during update of user")
356 366 raise JSONRPCError('failed to update user `%s`' % (userid,))
357 367
358 368
359 369 @jsonrpc_method()
360 370 def delete_user(request, apiuser, userid):
361 371 """
362 372 Deletes the specified user from the |RCE| user database.
363 373
364 374 This command can only be run using an |authtoken| with admin rights to
365 375 the specified repository.
366 376
367 377 .. important::
368 378
369 379 Ensure all open pull requests and open code review
370 380 requests to this user are close.
371 381
372 382 Also ensure all repositories, or repository groups owned by this
373 383 user are reassigned before deletion.
374 384
375 385 This command takes the following options:
376 386
377 387 :param apiuser: This is filled automatically from the |authtoken|.
378 388 :type apiuser: AuthUser
379 389 :param userid: Set the user to delete.
380 390 :type userid: str or int
381 391
382 392 Example output:
383 393
384 394 .. code-block:: bash
385 395
386 396 id : <id_given_in_input>
387 397 result: {
388 398 "msg" : "deleted user ID:<userid> <username>",
389 399 "user": null
390 400 }
391 401 error: null
392 402
393 403 Example error output:
394 404
395 405 .. code-block:: bash
396 406
397 407 id : <id_given_in_input>
398 408 result : null
399 409 error : {
400 410 "failed to delete user ID:<userid> <username>"
401 411 }
402 412
403 413 """
404 414 if not has_superadmin_permission(apiuser):
405 415 raise JSONRPCForbidden()
406 416
407 417 user = get_user_or_error(userid)
408
418 old_data = user.get_api_data()
409 419 try:
410 420 UserModel().delete(userid)
421 audit_logger.store_api(
422 'user.delete', action_data={'old_data': old_data},
423 user=apiuser)
424
411 425 Session().commit()
412 426 return {
413 427 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),
414 428 'user': None
415 429 }
416 430 except Exception:
417 431 log.exception("Error occurred during deleting of user")
418 432 raise JSONRPCError(
419 433 'failed to delete user ID:%s %s' % (user.user_id, user.username))
420 434
421 435
422 436 @jsonrpc_method()
423 437 def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
424 438 """
425 439 Displays all repositories locked by the specified user.
426 440
427 441 * If this command is run by a non-admin user, it returns
428 442 a list of |repos| locked by that user.
429 443
430 444 This command takes the following options:
431 445
432 446 :param apiuser: This is filled automatically from the |authtoken|.
433 447 :type apiuser: AuthUser
434 448 :param userid: Sets the userid whose list of locked |repos| will be
435 449 displayed.
436 450 :type userid: Optional(str or int)
437 451
438 452 Example output:
439 453
440 454 .. code-block:: bash
441 455
442 456 id : <id_given_in_input>
443 457 result : {
444 458 [repo_object, repo_object,...]
445 459 }
446 460 error : null
447 461 """
448 462
449 463 include_secrets = False
450 464 if not has_superadmin_permission(apiuser):
451 465 # make sure normal user does not pass someone else userid,
452 466 # he is not allowed to do that
453 467 if not isinstance(userid, Optional) and userid != apiuser.user_id:
454 468 raise JSONRPCError('userid is not the same as your user')
455 469 else:
456 470 include_secrets = True
457 471
458 472 userid = Optional.extract(userid, evaluate_locals=locals())
459 473 userid = getattr(userid, 'user_id', userid)
460 474 user = get_user_or_error(userid)
461 475
462 476 ret = []
463 477
464 478 # show all locks
465 479 for r in Repository.getAll():
466 480 _user_id, _time, _reason = r.locked
467 481 if _user_id and _time:
468 482 _api_data = r.get_api_data(include_secrets=include_secrets)
469 483 # if we use user filter just show the locks for this user
470 484 if safe_int(_user_id) == user.user_id:
471 485 ret.append(_api_data)
472 486
473 487 return ret
474 488
475 489
476 490 @jsonrpc_method()
477 491 def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))):
478 492 """
479 493 Fetches all action logs made by the specified user.
480 494
481 495 This command takes the following options:
482 496
483 497 :param apiuser: This is filled automatically from the |authtoken|.
484 498 :type apiuser: AuthUser
485 499 :param userid: Sets the userid whose list of locked |repos| will be
486 500 displayed.
487 501 :type userid: Optional(str or int)
488 502
489 503 Example output:
490 504
491 505 .. code-block:: bash
492 506
493 507 id : <id_given_in_input>
494 508 result : {
495 509 [action, action,...]
496 510 }
497 511 error : null
498 512 """
499 513
500 514 if not has_superadmin_permission(apiuser):
501 515 # make sure normal user does not pass someone else userid,
502 516 # he is not allowed to do that
503 517 if not isinstance(userid, Optional) and userid != apiuser.user_id:
504 518 raise JSONRPCError('userid is not the same as your user')
505 519
506 520 userid = Optional.extract(userid, evaluate_locals=locals())
507 521 userid = getattr(userid, 'user_id', userid)
508 522 user = get_user_or_error(userid)
509 523
510 524 ret = []
511 525
512 526 # show all user actions
513 527 for entry in UserModel().get_user_log(user, filter_term=None):
514 528 ret.append(entry)
515 529 return ret
@@ -1,773 +1,799 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
24 24 from rhodecode.api.utils import (
25 25 Optional, OAttr, store_update, has_superadmin_permission, get_origin,
26 26 get_user_or_error, get_user_group_or_error, get_perm_or_error)
27 from rhodecode.lib import audit_logger
27 28 from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi
28 29 from rhodecode.lib.exceptions import UserGroupAssignedException
29 30 from rhodecode.model.db import Session
30 31 from rhodecode.model.scm import UserGroupList
31 32 from rhodecode.model.user_group import UserGroupModel
32 33
33 34 log = logging.getLogger(__name__)
34 35
35 36
36 37 @jsonrpc_method()
37 38 def get_user_group(request, apiuser, usergroupid):
38 39 """
39 40 Returns the data of an existing user group.
40 41
41 42 This command can only be run using an |authtoken| with admin rights to
42 43 the specified repository.
43 44
44 45 :param apiuser: This is filled automatically from the |authtoken|.
45 46 :type apiuser: AuthUser
46 47 :param usergroupid: Set the user group from which to return data.
47 48 :type usergroupid: str or int
48 49
49 50 Example error output:
50 51
51 52 .. code-block:: bash
52 53
53 54 {
54 55 "error": null,
55 56 "id": <id>,
56 57 "result": {
57 58 "active": true,
58 59 "group_description": "group description",
59 60 "group_name": "group name",
60 61 "members": [
61 62 {
62 63 "name": "owner-name",
63 64 "origin": "owner",
64 65 "permission": "usergroup.admin",
65 66 "type": "user"
66 67 },
67 68 {
68 69 {
69 70 "name": "user name",
70 71 "origin": "permission",
71 72 "permission": "usergroup.admin",
72 73 "type": "user"
73 74 },
74 75 {
75 76 "name": "user group name",
76 77 "origin": "permission",
77 78 "permission": "usergroup.write",
78 79 "type": "user_group"
79 80 }
80 81 ],
81 82 "owner": "owner name",
82 83 "users": [],
83 84 "users_group_id": 2
84 85 }
85 86 }
86 87
87 88 """
88 89
89 90 user_group = get_user_group_or_error(usergroupid)
90 91 if not has_superadmin_permission(apiuser):
91 92 # check if we have at least read permission for this user group !
92 93 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
93 94 if not HasUserGroupPermissionAnyApi(*_perms)(
94 95 user=apiuser, user_group_name=user_group.users_group_name):
95 96 raise JSONRPCError('user group `%s` does not exist' % (
96 97 usergroupid,))
97 98
98 99 permissions = []
99 100 for _user in user_group.permissions():
100 101 user_data = {
101 102 'name': _user.username,
102 103 'permission': _user.permission,
103 104 'origin': get_origin(_user),
104 105 'type': "user",
105 106 }
106 107 permissions.append(user_data)
107 108
108 109 for _user_group in user_group.permission_user_groups():
109 110 user_group_data = {
110 111 'name': _user_group.users_group_name,
111 112 'permission': _user_group.permission,
112 113 'origin': get_origin(_user_group),
113 114 'type': "user_group",
114 115 }
115 116 permissions.append(user_group_data)
116 117
117 118 data = user_group.get_api_data()
118 119 data['members'] = permissions
119 120
120 121 return data
121 122
122 123
123 124 @jsonrpc_method()
124 125 def get_user_groups(request, apiuser):
125 126 """
126 127 Lists all the existing user groups within RhodeCode.
127 128
128 129 This command can only be run using an |authtoken| with admin rights to
129 130 the specified repository.
130 131
131 132 This command takes the following options:
132 133
133 134 :param apiuser: This is filled automatically from the |authtoken|.
134 135 :type apiuser: AuthUser
135 136
136 137 Example error output:
137 138
138 139 .. code-block:: bash
139 140
140 141 id : <id_given_in_input>
141 142 result : [<user_group_obj>,...]
142 143 error : null
143 144 """
144 145
145 146 include_secrets = has_superadmin_permission(apiuser)
146 147
147 148 result = []
148 149 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
149 150 extras = {'user': apiuser}
150 151 for user_group in UserGroupList(UserGroupModel().get_all(),
151 152 perm_set=_perms, extra_kwargs=extras):
152 153 result.append(
153 154 user_group.get_api_data(include_secrets=include_secrets))
154 155 return result
155 156
156 157
157 158 @jsonrpc_method()
158 159 def create_user_group(
159 160 request, apiuser, group_name, description=Optional(''),
160 161 owner=Optional(OAttr('apiuser')), active=Optional(True)):
161 162 """
162 163 Creates a new user group.
163 164
164 165 This command can only be run using an |authtoken| with admin rights to
165 166 the specified repository.
166 167
167 168 This command takes the following options:
168 169
169 170 :param apiuser: This is filled automatically from the |authtoken|.
170 171 :type apiuser: AuthUser
171 172 :param group_name: Set the name of the new user group.
172 173 :type group_name: str
173 174 :param description: Give a description of the new user group.
174 175 :type description: str
175 176 :param owner: Set the owner of the new user group.
176 177 If not set, the owner is the |authtoken| user.
177 178 :type owner: Optional(str or int)
178 179 :param active: Set this group as active.
179 180 :type active: Optional(``True`` | ``False``)
180 181
181 182 Example output:
182 183
183 184 .. code-block:: bash
184 185
185 186 id : <id_given_in_input>
186 187 result: {
187 188 "msg": "created new user group `<groupname>`",
188 189 "user_group": <user_group_object>
189 190 }
190 191 error: null
191 192
192 193 Example error output:
193 194
194 195 .. code-block:: bash
195 196
196 197 id : <id_given_in_input>
197 198 result : null
198 199 error : {
199 200 "user group `<group name>` already exist"
200 201 or
201 202 "failed to create group `<group name>`"
202 203 }
203 204
204 205 """
205 206
206 207 if not has_superadmin_permission(apiuser):
207 208 if not HasPermissionAnyApi('hg.usergroup.create.true')(user=apiuser):
208 209 raise JSONRPCForbidden()
209 210
210 211 if UserGroupModel().get_by_name(group_name):
211 212 raise JSONRPCError("user group `%s` already exist" % (group_name,))
212 213
213 214 try:
214 215 if isinstance(owner, Optional):
215 216 owner = apiuser.user_id
216 217
217 218 owner = get_user_or_error(owner)
218 219 active = Optional.extract(active)
219 220 description = Optional.extract(description)
220 ug = UserGroupModel().create(
221 user_group = UserGroupModel().create(
221 222 name=group_name, description=description, owner=owner,
222 223 active=active)
224 Session().flush()
225 creation_data = user_group.get_api_data()
226 audit_logger.store_api(
227 'user_group.create', action_data={'data': creation_data},
228 user=apiuser)
223 229 Session().commit()
224 230 return {
225 231 'msg': 'created new user group `%s`' % group_name,
226 'user_group': ug.get_api_data()
232 'user_group': creation_data
227 233 }
228 234 except Exception:
229 235 log.exception("Error occurred during creation of user group")
230 236 raise JSONRPCError('failed to create group `%s`' % (group_name,))
231 237
232 238
233 239 @jsonrpc_method()
234 240 def update_user_group(request, apiuser, usergroupid, group_name=Optional(''),
235 241 description=Optional(''), owner=Optional(None),
236 242 active=Optional(True)):
237 243 """
238 244 Updates the specified `user group` with the details provided.
239 245
240 246 This command can only be run using an |authtoken| with admin rights to
241 247 the specified repository.
242 248
243 249 :param apiuser: This is filled automatically from the |authtoken|.
244 250 :type apiuser: AuthUser
245 251 :param usergroupid: Set the id of the `user group` to update.
246 252 :type usergroupid: str or int
247 253 :param group_name: Set the new name the `user group`
248 254 :type group_name: str
249 255 :param description: Give a description for the `user group`
250 256 :type description: str
251 257 :param owner: Set the owner of the `user group`.
252 258 :type owner: Optional(str or int)
253 259 :param active: Set the group as active.
254 260 :type active: Optional(``True`` | ``False``)
255 261
256 262 Example output:
257 263
258 264 .. code-block:: bash
259 265
260 266 id : <id_given_in_input>
261 267 result : {
262 268 "msg": 'updated user group ID:<user group id> <user group name>',
263 269 "user_group": <user_group_object>
264 270 }
265 271 error : null
266 272
267 273 Example error output:
268 274
269 275 .. code-block:: bash
270 276
271 277 id : <id_given_in_input>
272 278 result : null
273 279 error : {
274 280 "failed to update user group `<user group name>`"
275 281 }
276 282
277 283 """
278 284
279 285 user_group = get_user_group_or_error(usergroupid)
280 286 include_secrets = False
281 287 if not has_superadmin_permission(apiuser):
282 288 # check if we have admin permission for this user group !
283 289 _perms = ('usergroup.admin',)
284 290 if not HasUserGroupPermissionAnyApi(*_perms)(
285 291 user=apiuser, user_group_name=user_group.users_group_name):
286 292 raise JSONRPCError(
287 293 'user group `%s` does not exist' % (usergroupid,))
288 294 else:
289 295 include_secrets = True
290 296
291 297 if not isinstance(owner, Optional):
292 298 owner = get_user_or_error(owner)
293 299
300 old_data = user_group.get_api_data()
294 301 updates = {}
295 302 store_update(updates, group_name, 'users_group_name')
296 303 store_update(updates, description, 'user_group_description')
297 304 store_update(updates, owner, 'user')
298 305 store_update(updates, active, 'users_group_active')
299 306 try:
300 307 UserGroupModel().update(user_group, updates)
308 audit_logger.store_api(
309 'user_group.edit', action_data={'old_data': old_data},
310 user=apiuser)
301 311 Session().commit()
302 312 return {
303 313 'msg': 'updated user group ID:%s %s' % (
304 314 user_group.users_group_id, user_group.users_group_name),
305 315 'user_group': user_group.get_api_data(
306 316 include_secrets=include_secrets)
307 317 }
308 318 except Exception:
309 319 log.exception("Error occurred during update of user group")
310 320 raise JSONRPCError(
311 321 'failed to update user group `%s`' % (usergroupid,))
312 322
313 323
314 324 @jsonrpc_method()
315 325 def delete_user_group(request, apiuser, usergroupid):
316 326 """
317 327 Deletes the specified `user group`.
318 328
319 329 This command can only be run using an |authtoken| with admin rights to
320 330 the specified repository.
321 331
322 332 This command takes the following options:
323 333
324 334 :param apiuser: filled automatically from apikey
325 335 :type apiuser: AuthUser
326 336 :param usergroupid:
327 337 :type usergroupid: int
328 338
329 339 Example output:
330 340
331 341 .. code-block:: bash
332 342
333 343 id : <id_given_in_input>
334 344 result : {
335 345 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
336 346 }
337 347 error : null
338 348
339 349 Example error output:
340 350
341 351 .. code-block:: bash
342 352
343 353 id : <id_given_in_input>
344 354 result : null
345 355 error : {
346 356 "failed to delete user group ID:<user_group_id> <user_group_name>"
347 357 or
348 358 "RepoGroup assigned to <repo_groups_list>"
349 359 }
350 360
351 361 """
352 362
353 363 user_group = get_user_group_or_error(usergroupid)
354 364 if not has_superadmin_permission(apiuser):
355 365 # check if we have admin permission for this user group !
356 366 _perms = ('usergroup.admin',)
357 367 if not HasUserGroupPermissionAnyApi(*_perms)(
358 368 user=apiuser, user_group_name=user_group.users_group_name):
359 369 raise JSONRPCError(
360 370 'user group `%s` does not exist' % (usergroupid,))
361 371
372 old_data = user_group.get_api_data()
362 373 try:
363 374 UserGroupModel().delete(user_group)
375 audit_logger.store_api(
376 'user_group.delete', action_data={'old_data': old_data},
377 user=apiuser)
364 378 Session().commit()
365 379 return {
366 380 'msg': 'deleted user group ID:%s %s' % (
367 381 user_group.users_group_id, user_group.users_group_name),
368 382 'user_group': None
369 383 }
370 384 except UserGroupAssignedException as e:
371 385 log.exception("UserGroupAssigned error")
372 386 raise JSONRPCError(str(e))
373 387 except Exception:
374 388 log.exception("Error occurred during deletion of user group")
375 389 raise JSONRPCError(
376 390 'failed to delete user group ID:%s %s' %(
377 391 user_group.users_group_id, user_group.users_group_name))
378 392
379 393
380 394 @jsonrpc_method()
381 395 def add_user_to_user_group(request, apiuser, usergroupid, userid):
382 396 """
383 397 Adds a user to a `user group`. If the user already exists in the group
384 398 this command will return false.
385 399
386 400 This command can only be run using an |authtoken| with admin rights to
387 401 the specified user group.
388 402
389 403 This command takes the following options:
390 404
391 405 :param apiuser: This is filled automatically from the |authtoken|.
392 406 :type apiuser: AuthUser
393 407 :param usergroupid: Set the name of the `user group` to which a
394 408 user will be added.
395 409 :type usergroupid: int
396 410 :param userid: Set the `user_id` of the user to add to the group.
397 411 :type userid: int
398 412
399 413 Example output:
400 414
401 415 .. code-block:: bash
402 416
403 417 id : <id_given_in_input>
404 418 result : {
405 419 "success": True|False # depends on if member is in group
406 420 "msg": "added member `<username>` to user group `<groupname>` |
407 421 User is already in that group"
408 422
409 423 }
410 424 error : null
411 425
412 426 Example error output:
413 427
414 428 .. code-block:: bash
415 429
416 430 id : <id_given_in_input>
417 431 result : null
418 432 error : {
419 433 "failed to add member to user group `<user_group_name>`"
420 434 }
421 435
422 436 """
423 437
424 438 user = get_user_or_error(userid)
425 439 user_group = get_user_group_or_error(usergroupid)
426 440 if not has_superadmin_permission(apiuser):
427 441 # check if we have admin permission for this user group !
428 442 _perms = ('usergroup.admin',)
429 443 if not HasUserGroupPermissionAnyApi(*_perms)(
430 444 user=apiuser, user_group_name=user_group.users_group_name):
431 445 raise JSONRPCError('user group `%s` does not exist' % (
432 446 usergroupid,))
433 447
434 448 try:
435 449 ugm = UserGroupModel().add_user_to_group(user_group, user)
436 450 success = True if ugm is not True else False
437 451 msg = 'added member `%s` to user group `%s`' % (
438 452 user.username, user_group.users_group_name
439 453 )
440 454 msg = msg if success else 'User is already in that group'
455 if success:
456 user_data = user.get_api_data()
457 audit_logger.store_api(
458 'user_group.edit.member.add', action_data={'user': user_data},
459 user=apiuser)
460
441 461 Session().commit()
442 462
443 463 return {
444 464 'success': success,
445 465 'msg': msg
446 466 }
447 467 except Exception:
448 468 log.exception("Error occurred during adding a member to user group")
449 469 raise JSONRPCError(
450 470 'failed to add member to user group `%s`' % (
451 471 user_group.users_group_name,
452 472 )
453 473 )
454 474
455 475
456 476 @jsonrpc_method()
457 477 def remove_user_from_user_group(request, apiuser, usergroupid, userid):
458 478 """
459 479 Removes a user from a user group.
460 480
461 481 * If the specified user is not in the group, this command will return
462 482 `false`.
463 483
464 484 This command can only be run using an |authtoken| with admin rights to
465 485 the specified user group.
466 486
467 487 :param apiuser: This is filled automatically from the |authtoken|.
468 488 :type apiuser: AuthUser
469 489 :param usergroupid: Sets the user group name.
470 490 :type usergroupid: str or int
471 491 :param userid: The user you wish to remove from |RCE|.
472 492 :type userid: str or int
473 493
474 494 Example output:
475 495
476 496 .. code-block:: bash
477 497
478 498 id : <id_given_in_input>
479 499 result: {
480 500 "success": True|False, # depends on if member is in group
481 501 "msg": "removed member <username> from user group <groupname> |
482 502 User wasn't in group"
483 503 }
484 504 error: null
485 505
486 506 """
487 507
488 508 user = get_user_or_error(userid)
489 509 user_group = get_user_group_or_error(usergroupid)
490 510 if not has_superadmin_permission(apiuser):
491 511 # check if we have admin permission for this user group !
492 512 _perms = ('usergroup.admin',)
493 513 if not HasUserGroupPermissionAnyApi(*_perms)(
494 514 user=apiuser, user_group_name=user_group.users_group_name):
495 515 raise JSONRPCError(
496 516 'user group `%s` does not exist' % (usergroupid,))
497 517
498 518 try:
499 519 success = UserGroupModel().remove_user_from_group(user_group, user)
500 520 msg = 'removed member `%s` from user group `%s`' % (
501 521 user.username, user_group.users_group_name
502 522 )
503 523 msg = msg if success else "User wasn't in group"
524 if success:
525 user_data = user.get_api_data()
526 audit_logger.store_api(
527 'user_group.edit.member.delete', action_data={'user': user_data},
528 user=apiuser)
529
504 530 Session().commit()
505 531 return {'success': success, 'msg': msg}
506 532 except Exception:
507 533 log.exception("Error occurred during removing an member from user group")
508 534 raise JSONRPCError(
509 535 'failed to remove member from user group `%s`' % (
510 536 user_group.users_group_name,
511 537 )
512 538 )
513 539
514 540
515 541 @jsonrpc_method()
516 542 def grant_user_permission_to_user_group(
517 543 request, apiuser, usergroupid, userid, perm):
518 544 """
519 545 Set permissions for a user in a user group.
520 546
521 547 :param apiuser: This is filled automatically from the |authtoken|.
522 548 :type apiuser: AuthUser
523 549 :param usergroupid: Set the user group to edit permissions on.
524 550 :type usergroupid: str or int
525 551 :param userid: Set the user from whom you wish to set permissions.
526 552 :type userid: str
527 553 :param perm: (usergroup.(none|read|write|admin))
528 554 :type perm: str
529 555
530 556 Example output:
531 557
532 558 .. code-block:: bash
533 559
534 560 id : <id_given_in_input>
535 561 result : {
536 562 "msg": "Granted perm: `<perm_name>` for user: `<username>` in user group: `<user_group_name>`",
537 563 "success": true
538 564 }
539 565 error : null
540 566 """
541 567
542 568 user_group = get_user_group_or_error(usergroupid)
543 569
544 570 if not has_superadmin_permission(apiuser):
545 571 # check if we have admin permission for this user group !
546 572 _perms = ('usergroup.admin',)
547 573 if not HasUserGroupPermissionAnyApi(*_perms)(
548 574 user=apiuser, user_group_name=user_group.users_group_name):
549 575 raise JSONRPCError(
550 576 'user group `%s` does not exist' % (usergroupid,))
551 577
552 578 user = get_user_or_error(userid)
553 579 perm = get_perm_or_error(perm, prefix='usergroup.')
554 580
555 581 try:
556 582 UserGroupModel().grant_user_permission(
557 583 user_group=user_group, user=user, perm=perm)
558 584 Session().commit()
559 585 return {
560 586 'msg':
561 587 'Granted perm: `%s` for user: `%s` in user group: `%s`' % (
562 588 perm.permission_name, user.username,
563 589 user_group.users_group_name
564 590 ),
565 591 'success': True
566 592 }
567 593 except Exception:
568 594 log.exception("Error occurred during editing permissions "
569 595 "for user in user group")
570 596 raise JSONRPCError(
571 597 'failed to edit permission for user: '
572 598 '`%s` in user group: `%s`' % (
573 599 userid, user_group.users_group_name))
574 600
575 601
576 602 @jsonrpc_method()
577 603 def revoke_user_permission_from_user_group(
578 604 request, apiuser, usergroupid, userid):
579 605 """
580 606 Revoke a users permissions in a user group.
581 607
582 608 :param apiuser: This is filled automatically from the |authtoken|.
583 609 :type apiuser: AuthUser
584 610 :param usergroupid: Set the user group from which to revoke the user
585 611 permissions.
586 612 :type: usergroupid: str or int
587 613 :param userid: Set the userid of the user whose permissions will be
588 614 revoked.
589 615 :type userid: str
590 616
591 617 Example output:
592 618
593 619 .. code-block:: bash
594 620
595 621 id : <id_given_in_input>
596 622 result : {
597 623 "msg": "Revoked perm for user: `<username>` in user group: `<user_group_name>`",
598 624 "success": true
599 625 }
600 626 error : null
601 627 """
602 628
603 629 user_group = get_user_group_or_error(usergroupid)
604 630
605 631 if not has_superadmin_permission(apiuser):
606 632 # check if we have admin permission for this user group !
607 633 _perms = ('usergroup.admin',)
608 634 if not HasUserGroupPermissionAnyApi(*_perms)(
609 635 user=apiuser, user_group_name=user_group.users_group_name):
610 636 raise JSONRPCError(
611 637 'user group `%s` does not exist' % (usergroupid,))
612 638
613 639 user = get_user_or_error(userid)
614 640
615 641 try:
616 642 UserGroupModel().revoke_user_permission(
617 643 user_group=user_group, user=user)
618 644 Session().commit()
619 645 return {
620 646 'msg': 'Revoked perm for user: `%s` in user group: `%s`' % (
621 647 user.username, user_group.users_group_name
622 648 ),
623 649 'success': True
624 650 }
625 651 except Exception:
626 652 log.exception("Error occurred during editing permissions "
627 653 "for user in user group")
628 654 raise JSONRPCError(
629 655 'failed to edit permission for user: `%s` in user group: `%s`'
630 656 % (userid, user_group.users_group_name))
631 657
632 658
633 659 @jsonrpc_method()
634 660 def grant_user_group_permission_to_user_group(
635 661 request, apiuser, usergroupid, sourceusergroupid, perm):
636 662 """
637 663 Give one user group permissions to another user group.
638 664
639 665 :param apiuser: This is filled automatically from the |authtoken|.
640 666 :type apiuser: AuthUser
641 667 :param usergroupid: Set the user group on which to edit permissions.
642 668 :type usergroupid: str or int
643 669 :param sourceusergroupid: Set the source user group to which
644 670 access/permissions will be granted.
645 671 :type sourceusergroupid: str or int
646 672 :param perm: (usergroup.(none|read|write|admin))
647 673 :type perm: str
648 674
649 675 Example output:
650 676
651 677 .. code-block:: bash
652 678
653 679 id : <id_given_in_input>
654 680 result : {
655 681 "msg": "Granted perm: `<perm_name>` for user group: `<source_user_group_name>` in user group: `<user_group_name>`",
656 682 "success": true
657 683 }
658 684 error : null
659 685 """
660 686
661 687 user_group = get_user_group_or_error(sourceusergroupid)
662 688 target_user_group = get_user_group_or_error(usergroupid)
663 689 perm = get_perm_or_error(perm, prefix='usergroup.')
664 690
665 691 if not has_superadmin_permission(apiuser):
666 692 # check if we have admin permission for this user group !
667 693 _perms = ('usergroup.admin',)
668 694 if not HasUserGroupPermissionAnyApi(*_perms)(
669 695 user=apiuser,
670 696 user_group_name=target_user_group.users_group_name):
671 697 raise JSONRPCError(
672 698 'to user group `%s` does not exist' % (usergroupid,))
673 699
674 700 # check if we have at least read permission for source user group !
675 701 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
676 702 if not HasUserGroupPermissionAnyApi(*_perms)(
677 703 user=apiuser, user_group_name=user_group.users_group_name):
678 704 raise JSONRPCError(
679 705 'user group `%s` does not exist' % (sourceusergroupid,))
680 706
681 707 try:
682 708 UserGroupModel().grant_user_group_permission(
683 709 target_user_group=target_user_group,
684 710 user_group=user_group, perm=perm)
685 711 Session().commit()
686 712
687 713 return {
688 714 'msg': 'Granted perm: `%s` for user group: `%s` '
689 715 'in user group: `%s`' % (
690 716 perm.permission_name, user_group.users_group_name,
691 717 target_user_group.users_group_name
692 718 ),
693 719 'success': True
694 720 }
695 721 except Exception:
696 722 log.exception("Error occurred during editing permissions "
697 723 "for user group in user group")
698 724 raise JSONRPCError(
699 725 'failed to edit permission for user group: `%s` in '
700 726 'user group: `%s`' % (
701 727 sourceusergroupid, target_user_group.users_group_name
702 728 )
703 729 )
704 730
705 731
706 732 @jsonrpc_method()
707 733 def revoke_user_group_permission_from_user_group(
708 734 request, apiuser, usergroupid, sourceusergroupid):
709 735 """
710 736 Revoke the permissions that one user group has to another.
711 737
712 738 :param apiuser: This is filled automatically from the |authtoken|.
713 739 :type apiuser: AuthUser
714 740 :param usergroupid: Set the user group on which to edit permissions.
715 741 :type usergroupid: str or int
716 742 :param sourceusergroupid: Set the user group from which permissions
717 743 are revoked.
718 744 :type sourceusergroupid: str or int
719 745
720 746 Example output:
721 747
722 748 .. code-block:: bash
723 749
724 750 id : <id_given_in_input>
725 751 result : {
726 752 "msg": "Revoked perm for user group: `<user_group_name>` in user group: `<target_user_group_name>`",
727 753 "success": true
728 754 }
729 755 error : null
730 756 """
731 757
732 758 user_group = get_user_group_or_error(sourceusergroupid)
733 759 target_user_group = get_user_group_or_error(usergroupid)
734 760
735 761 if not has_superadmin_permission(apiuser):
736 762 # check if we have admin permission for this user group !
737 763 _perms = ('usergroup.admin',)
738 764 if not HasUserGroupPermissionAnyApi(*_perms)(
739 765 user=apiuser,
740 766 user_group_name=target_user_group.users_group_name):
741 767 raise JSONRPCError(
742 768 'to user group `%s` does not exist' % (usergroupid,))
743 769
744 770 # check if we have at least read permission
745 771 # for the source user group !
746 772 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
747 773 if not HasUserGroupPermissionAnyApi(*_perms)(
748 774 user=apiuser, user_group_name=user_group.users_group_name):
749 775 raise JSONRPCError(
750 776 'user group `%s` does not exist' % (sourceusergroupid,))
751 777
752 778 try:
753 779 UserGroupModel().revoke_user_group_permission(
754 780 target_user_group=target_user_group, user_group=user_group)
755 781 Session().commit()
756 782
757 783 return {
758 784 'msg': 'Revoked perm for user group: '
759 785 '`%s` in user group: `%s`' % (
760 786 user_group.users_group_name,
761 787 target_user_group.users_group_name
762 788 ),
763 789 'success': True
764 790 }
765 791 except Exception:
766 792 log.exception("Error occurred during editing permissions "
767 793 "for user group in user group")
768 794 raise JSONRPCError(
769 795 'failed to edit permission for user group: '
770 796 '`%s` in user group: `%s`' % (
771 797 sourceusergroupid, target_user_group.users_group_name
772 798 )
773 799 )
@@ -1,509 +1,505 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from sqlalchemy.sql.functions import coalesce
28 28
29 29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 30
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils import PartialRenderer
37 37 from rhodecode.lib.utils2 import safe_int, safe_unicode
38 38 from rhodecode.model.auth_token import AuthTokenModel
39 39 from rhodecode.model.user import UserModel
40 40 from rhodecode.model.user_group import UserGroupModel
41 41 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
42 42 from rhodecode.model.meta import Session
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class AdminUsersView(BaseAppView, DataGridAppView):
48 48 ALLOW_SCOPED_TOKENS = False
49 49 """
50 50 This view has alternative version inside EE, if modified please take a look
51 51 in there as well.
52 52 """
53 53
54 54 def load_default_context(self):
55 55 c = self._get_local_tmpl_context()
56 56 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
57 57 self._register_global_c(c)
58 58 return c
59 59
60 60 def _redirect_for_default_user(self, username):
61 61 _ = self.request.translate
62 62 if username == User.DEFAULT_USER:
63 63 h.flash(_("You can't edit this user"), category='warning')
64 64 # TODO(marcink): redirect to 'users' admin panel once this
65 65 # is a pyramid view
66 66 raise HTTPFound('/')
67 67
68 68 @HasPermissionAllDecorator('hg.admin')
69 69 @view_config(
70 70 route_name='users', request_method='GET',
71 71 renderer='rhodecode:templates/admin/users/users.mako')
72 72 def users_list(self):
73 73 c = self.load_default_context()
74 74 return self._get_template_context(c)
75 75
76 76 @HasPermissionAllDecorator('hg.admin')
77 77 @view_config(
78 78 # renderer defined below
79 79 route_name='users_data', request_method='GET',
80 80 renderer='json_ext', xhr=True)
81 81 def users_list_data(self):
82 82 draw, start, limit = self._extract_chunk(self.request)
83 83 search_q, order_by, order_dir = self._extract_ordering(self.request)
84 84
85 85 _render = PartialRenderer('data_table/_dt_elements.mako')
86 86
87 87 def user_actions(user_id, username):
88 88 return _render("user_actions", user_id, username)
89 89
90 90 users_data_total_count = User.query()\
91 91 .filter(User.username != User.DEFAULT_USER) \
92 92 .count()
93 93
94 94 # json generate
95 95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
96 96
97 97 if search_q:
98 98 like_expression = u'%{}%'.format(safe_unicode(search_q))
99 99 base_q = base_q.filter(or_(
100 100 User.username.ilike(like_expression),
101 101 User._email.ilike(like_expression),
102 102 User.name.ilike(like_expression),
103 103 User.lastname.ilike(like_expression),
104 104 ))
105 105
106 106 users_data_total_filtered_count = base_q.count()
107 107
108 108 sort_col = getattr(User, order_by, None)
109 109 if sort_col:
110 110 if order_dir == 'asc':
111 111 # handle null values properly to order by NULL last
112 112 if order_by in ['last_activity']:
113 113 sort_col = coalesce(sort_col, datetime.date.max)
114 114 sort_col = sort_col.asc()
115 115 else:
116 116 # handle null values properly to order by NULL last
117 117 if order_by in ['last_activity']:
118 118 sort_col = coalesce(sort_col, datetime.date.min)
119 119 sort_col = sort_col.desc()
120 120
121 121 base_q = base_q.order_by(sort_col)
122 122 base_q = base_q.offset(start).limit(limit)
123 123
124 124 users_list = base_q.all()
125 125
126 126 users_data = []
127 127 for user in users_list:
128 128 users_data.append({
129 129 "username": h.gravatar_with_user(user.username),
130 130 "email": user.email,
131 131 "first_name": user.first_name,
132 132 "last_name": user.last_name,
133 133 "last_login": h.format_date(user.last_login),
134 134 "last_activity": h.format_date(user.last_activity),
135 135 "active": h.bool2icon(user.active),
136 136 "active_raw": user.active,
137 137 "admin": h.bool2icon(user.admin),
138 138 "extern_type": user.extern_type,
139 139 "extern_name": user.extern_name,
140 140 "action": user_actions(user.user_id, user.username),
141 141 })
142 142
143 143 data = ({
144 144 'draw': draw,
145 145 'data': users_data,
146 146 'recordsTotal': users_data_total_count,
147 147 'recordsFiltered': users_data_total_filtered_count,
148 148 })
149 149
150 150 return data
151 151
152 152 @LoginRequired()
153 153 @HasPermissionAllDecorator('hg.admin')
154 154 @view_config(
155 155 route_name='edit_user_auth_tokens', request_method='GET',
156 156 renderer='rhodecode:templates/admin/users/user_edit.mako')
157 157 def auth_tokens(self):
158 158 _ = self.request.translate
159 159 c = self.load_default_context()
160 160
161 161 user_id = self.request.matchdict.get('user_id')
162 162 c.user = User.get_or_404(user_id, pyramid_exc=True)
163 163 self._redirect_for_default_user(c.user.username)
164 164
165 165 c.active = 'auth_tokens'
166 166
167 167 c.lifetime_values = [
168 168 (str(-1), _('forever')),
169 169 (str(5), _('5 minutes')),
170 170 (str(60), _('1 hour')),
171 171 (str(60 * 24), _('1 day')),
172 172 (str(60 * 24 * 30), _('1 month')),
173 173 ]
174 174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
175 175 c.role_values = [
176 176 (x, AuthTokenModel.cls._get_role_name(x))
177 177 for x in AuthTokenModel.cls.ROLES]
178 178 c.role_options = [(c.role_values, _("Role"))]
179 179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
180 180 c.user.user_id, show_expired=True)
181 181 return self._get_template_context(c)
182 182
183 183 def maybe_attach_token_scope(self, token):
184 184 # implemented in EE edition
185 185 pass
186 186
187 187 @LoginRequired()
188 188 @HasPermissionAllDecorator('hg.admin')
189 189 @CSRFRequired()
190 190 @view_config(
191 191 route_name='edit_user_auth_tokens_add', request_method='POST')
192 192 def auth_tokens_add(self):
193 193 _ = self.request.translate
194 194 c = self.load_default_context()
195 195
196 196 user_id = self.request.matchdict.get('user_id')
197 197 c.user = User.get_or_404(user_id, pyramid_exc=True)
198 198
199 199 self._redirect_for_default_user(c.user.username)
200 200
201 201 user_data = c.user.get_api_data()
202 202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
203 203 description = self.request.POST.get('description')
204 204 role = self.request.POST.get('role')
205 205
206 206 token = AuthTokenModel().create(
207 207 c.user.user_id, description, lifetime, role)
208 208 token_data = token.get_api_data()
209 209
210 210 self.maybe_attach_token_scope(token)
211 211 audit_logger.store_web(
212 action='user.edit.token.add',
213 action_data={'data': {'token': token_data, 'user': user_data}},
212 'user.edit.token.add', action_data={
213 'data': {'token': token_data, 'user': user_data}},
214 214 user=self._rhodecode_user, )
215 215 Session().commit()
216 216
217 217 h.flash(_("Auth token successfully created"), category='success')
218 218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
219 219
220 220 @LoginRequired()
221 221 @HasPermissionAllDecorator('hg.admin')
222 222 @CSRFRequired()
223 223 @view_config(
224 224 route_name='edit_user_auth_tokens_delete', request_method='POST')
225 225 def auth_tokens_delete(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 user_id = self.request.matchdict.get('user_id')
230 230 c.user = User.get_or_404(user_id, pyramid_exc=True)
231 231 self._redirect_for_default_user(c.user.username)
232 232 user_data = c.user.get_api_data()
233 233
234 234 del_auth_token = self.request.POST.get('del_auth_token')
235 235
236 236 if del_auth_token:
237 237 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
238 238 token_data = token.get_api_data()
239 239
240 240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 241 audit_logger.store_web(
242 action='user.edit.token.delete',
243 action_data={'data': {'token': token_data, 'user': user_data}},
242 'user.edit.token.delete', action_data={
243 'data': {'token': token_data, 'user': user_data}},
244 244 user=self._rhodecode_user,)
245 245 Session().commit()
246 246 h.flash(_("Auth token successfully deleted"), category='success')
247 247
248 248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
249 249
250 250 @LoginRequired()
251 251 @HasPermissionAllDecorator('hg.admin')
252 252 @view_config(
253 253 route_name='edit_user_emails', request_method='GET',
254 254 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 255 def emails(self):
256 256 _ = self.request.translate
257 257 c = self.load_default_context()
258 258
259 259 user_id = self.request.matchdict.get('user_id')
260 260 c.user = User.get_or_404(user_id, pyramid_exc=True)
261 261 self._redirect_for_default_user(c.user.username)
262 262
263 263 c.active = 'emails'
264 264 c.user_email_map = UserEmailMap.query() \
265 265 .filter(UserEmailMap.user == c.user).all()
266 266
267 267 return self._get_template_context(c)
268 268
269 269 @LoginRequired()
270 270 @HasPermissionAllDecorator('hg.admin')
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='edit_user_emails_add', request_method='POST')
274 274 def emails_add(self):
275 275 _ = self.request.translate
276 276 c = self.load_default_context()
277 277
278 278 user_id = self.request.matchdict.get('user_id')
279 279 c.user = User.get_or_404(user_id, pyramid_exc=True)
280 280 self._redirect_for_default_user(c.user.username)
281 281
282 282 email = self.request.POST.get('new_email')
283 283 user_data = c.user.get_api_data()
284 284 try:
285 285 UserModel().add_extra_email(c.user.user_id, email)
286 286 audit_logger.store_web(
287 'user.edit.email.add',
288 action_data={'email': email, 'user': user_data},
287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
289 288 user=self._rhodecode_user)
290 289 Session().commit()
291 290 h.flash(_("Added new email address `%s` for user account") % email,
292 291 category='success')
293 292 except formencode.Invalid as error:
294 293 h.flash(h.escape(error.error_dict['email']), category='error')
295 294 except Exception:
296 295 log.exception("Exception during email saving")
297 296 h.flash(_('An error occurred during email saving'),
298 297 category='error')
299 298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
300 299
301 300 @LoginRequired()
302 301 @HasPermissionAllDecorator('hg.admin')
303 302 @CSRFRequired()
304 303 @view_config(
305 304 route_name='edit_user_emails_delete', request_method='POST')
306 305 def emails_delete(self):
307 306 _ = self.request.translate
308 307 c = self.load_default_context()
309 308
310 309 user_id = self.request.matchdict.get('user_id')
311 310 c.user = User.get_or_404(user_id, pyramid_exc=True)
312 311 self._redirect_for_default_user(c.user.username)
313 312
314 313 email_id = self.request.POST.get('del_email_id')
315 314 user_model = UserModel()
316 315
317 316 email = UserEmailMap.query().get(email_id).email
318 317 user_data = c.user.get_api_data()
319 318 user_model.delete_extra_email(c.user.user_id, email_id)
320 319 audit_logger.store_web(
321 'user.edit.email.delete',
322 action_data={'email': email, 'user': user_data},
320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
323 321 user=self._rhodecode_user)
324 322 Session().commit()
325 323 h.flash(_("Removed email address from user account"),
326 324 category='success')
327 325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
328 326
329 327 @LoginRequired()
330 328 @HasPermissionAllDecorator('hg.admin')
331 329 @view_config(
332 330 route_name='edit_user_ips', request_method='GET',
333 331 renderer='rhodecode:templates/admin/users/user_edit.mako')
334 332 def ips(self):
335 333 _ = self.request.translate
336 334 c = self.load_default_context()
337 335
338 336 user_id = self.request.matchdict.get('user_id')
339 337 c.user = User.get_or_404(user_id, pyramid_exc=True)
340 338 self._redirect_for_default_user(c.user.username)
341 339
342 340 c.active = 'ips'
343 341 c.user_ip_map = UserIpMap.query() \
344 342 .filter(UserIpMap.user == c.user).all()
345 343
346 344 c.inherit_default_ips = c.user.inherit_default_permissions
347 345 c.default_user_ip_map = UserIpMap.query() \
348 346 .filter(UserIpMap.user == User.get_default_user()).all()
349 347
350 348 return self._get_template_context(c)
351 349
352 350 @LoginRequired()
353 351 @HasPermissionAllDecorator('hg.admin')
354 352 @CSRFRequired()
355 353 @view_config(
356 354 route_name='edit_user_ips_add', request_method='POST')
357 355 def ips_add(self):
358 356 _ = self.request.translate
359 357 c = self.load_default_context()
360 358
361 359 user_id = self.request.matchdict.get('user_id')
362 360 c.user = User.get_or_404(user_id, pyramid_exc=True)
363 361 # NOTE(marcink): this view is allowed for default users, as we can
364 362 # edit their IP white list
365 363
366 364 user_model = UserModel()
367 365 desc = self.request.POST.get('description')
368 366 try:
369 367 ip_list = user_model.parse_ip_range(
370 368 self.request.POST.get('new_ip'))
371 369 except Exception as e:
372 370 ip_list = []
373 371 log.exception("Exception during ip saving")
374 372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
375 373 category='error')
376 374 added = []
377 375 user_data = c.user.get_api_data()
378 376 for ip in ip_list:
379 377 try:
380 378 user_model.add_extra_ip(c.user.user_id, ip, desc)
381 379 audit_logger.store_web(
382 'user.edit.ip.add',
383 action_data={'ip': ip, 'user': user_data},
380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
384 381 user=self._rhodecode_user)
385 382 Session().commit()
386 383 added.append(ip)
387 384 except formencode.Invalid as error:
388 385 msg = error.error_dict['ip']
389 386 h.flash(msg, category='error')
390 387 except Exception:
391 388 log.exception("Exception during ip saving")
392 389 h.flash(_('An error occurred during ip saving'),
393 390 category='error')
394 391 if added:
395 392 h.flash(
396 393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
397 394 category='success')
398 395 if 'default_user' in self.request.POST:
399 396 # case for editing global IP list we do it for 'DEFAULT' user
400 397 raise HTTPFound(h.route_path('admin_permissions_ips'))
401 398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
402 399
403 400 @LoginRequired()
404 401 @HasPermissionAllDecorator('hg.admin')
405 402 @CSRFRequired()
406 403 @view_config(
407 404 route_name='edit_user_ips_delete', request_method='POST')
408 405 def ips_delete(self):
409 406 _ = self.request.translate
410 407 c = self.load_default_context()
411 408
412 409 user_id = self.request.matchdict.get('user_id')
413 410 c.user = User.get_or_404(user_id, pyramid_exc=True)
414 411 # NOTE(marcink): this view is allowed for default users, as we can
415 412 # edit their IP white list
416 413
417 414 ip_id = self.request.POST.get('del_ip_id')
418 415 user_model = UserModel()
419 416 user_data = c.user.get_api_data()
420 417 ip = UserIpMap.query().get(ip_id).ip_addr
421 418 user_model.delete_extra_ip(c.user.user_id, ip_id)
422 419 audit_logger.store_web(
423 'user.edit.ip.delete',
424 action_data={'ip': ip, 'user': user_data},
420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
425 421 user=self._rhodecode_user)
426 422 Session().commit()
427 423 h.flash(_("Removed ip address from user whitelist"), category='success')
428 424
429 425 if 'default_user' in self.request.POST:
430 426 # case for editing global IP list we do it for 'DEFAULT' user
431 427 raise HTTPFound(h.route_path('admin_permissions_ips'))
432 428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
433 429
434 430 @LoginRequired()
435 431 @HasPermissionAllDecorator('hg.admin')
436 432 @view_config(
437 433 route_name='edit_user_groups_management', request_method='GET',
438 434 renderer='rhodecode:templates/admin/users/user_edit.mako')
439 435 def groups_management(self):
440 436 c = self.load_default_context()
441 437
442 438 user_id = self.request.matchdict.get('user_id')
443 439 c.user = User.get_or_404(user_id, pyramid_exc=True)
444 440 c.data = c.user.group_member
445 441 self._redirect_for_default_user(c.user.username)
446 442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
447 443 for group in c.user.group_member]
448 444 c.groups = json.dumps(groups)
449 445 c.active = 'groups'
450 446
451 447 return self._get_template_context(c)
452 448
453 449 @LoginRequired()
454 450 @HasPermissionAllDecorator('hg.admin')
455 451 @CSRFRequired()
456 452 @view_config(
457 453 route_name='edit_user_groups_management_updates', request_method='POST')
458 454 def groups_management_updates(self):
459 455 _ = self.request.translate
460 456 c = self.load_default_context()
461 457
462 458 user_id = self.request.matchdict.get('user_id')
463 459 c.user = User.get_or_404(user_id, pyramid_exc=True)
464 460 self._redirect_for_default_user(c.user.username)
465 461
466 462 users_groups = set(self.request.POST.getall('users_group_id'))
467 463 users_groups_model = []
468 464
469 465 for ugid in users_groups:
470 466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
471 467 user_group_model = UserGroupModel()
472 468 user_group_model.change_groups(c.user, users_groups_model)
473 469
474 470 Session().commit()
475 471 c.active = 'user_groups_management'
476 472 h.flash(_("Groups successfully changed"), category='success')
477 473
478 474 return HTTPFound(h.route_path(
479 475 'edit_user_groups_management', user_id=user_id))
480 476
481 477 @LoginRequired()
482 478 @HasPermissionAllDecorator('hg.admin')
483 479 @view_config(
484 480 route_name='edit_user_audit_logs', request_method='GET',
485 481 renderer='rhodecode:templates/admin/users/user_edit.mako')
486 482 def user_audit_logs(self):
487 483 _ = self.request.translate
488 484 c = self.load_default_context()
489 485
490 486 user_id = self.request.matchdict.get('user_id')
491 487 c.user = User.get_or_404(user_id, pyramid_exc=True)
492 488 self._redirect_for_default_user(c.user.username)
493 489 c.active = 'audit'
494 490
495 491 p = safe_int(self.request.GET.get('page', 1), 1)
496 492
497 493 filter_term = self.request.GET.get('filter')
498 494 user_log = UserModel().get_user_log(c.user, filter_term)
499 495
500 496 def url_generator(**kw):
501 497 if filter_term:
502 498 kw['filter'] = filter_term
503 499 return self.request.current_route_path(_query=kw)
504 500
505 501 c.audit_logs = h.Page(
506 502 user_log, page=p, items_per_page=10, url=url_generator)
507 503 c.filter_term = filter_term
508 504 return self._get_template_context(c)
509 505
@@ -1,426 +1,425 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 25 import logging
26 26 import urlparse
27 27
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.view import view_config
30 30 from recaptcha.client.captcha import submit
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 34 from rhodecode.events import UserRegistered
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import audit_logger
37 37 from rhodecode.lib.auth import (
38 38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 39 from rhodecode.lib.base import get_ip_addr
40 40 from rhodecode.lib.exceptions import UserCreationError
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.model.db import User, UserApiKeys
43 43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.auth_token import AuthTokenModel
46 46 from rhodecode.model.settings import SettingsModel
47 47 from rhodecode.model.user import UserModel
48 48 from rhodecode.translation import _
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53 CaptchaData = collections.namedtuple(
54 54 'CaptchaData', 'active, private_key, public_key')
55 55
56 56
57 57 def _store_user_in_session(session, username, remember=False):
58 58 user = User.get_by_username(username, case_insensitive=True)
59 59 auth_user = AuthUser(user.user_id)
60 60 auth_user.set_authenticated()
61 61 cs = auth_user.get_cookie_store()
62 62 session['rhodecode_user'] = cs
63 63 user.update_lastlogin()
64 64 Session().commit()
65 65
66 66 # If they want to be remembered, update the cookie
67 67 if remember:
68 68 _year = (datetime.datetime.now() +
69 69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 70 session._set_cookie_expires(_year)
71 71
72 72 session.save()
73 73
74 74 safe_cs = cs.copy()
75 75 safe_cs['password'] = '****'
76 76 log.info('user %s is now authenticated and stored in '
77 77 'session, session attrs %s', username, safe_cs)
78 78
79 79 # dumps session attrs back to cookie
80 80 session._update_cookie_out()
81 81 # we set new cookie
82 82 headers = None
83 83 if session.request['set_cookie']:
84 84 # send set-cookie headers back to response to update cookie
85 85 headers = [('Set-Cookie', session.request['cookie_out'])]
86 86 return headers
87 87
88 88
89 89 def get_came_from(request):
90 90 came_from = safe_str(request.GET.get('came_from', ''))
91 91 parsed = urlparse.urlparse(came_from)
92 92 allowed_schemes = ['http', 'https']
93 93 default_came_from = h.route_path('home')
94 94 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 95 log.error('Suspicious URL scheme detected %s for url %s' %
96 96 (parsed.scheme, parsed))
97 97 came_from = default_came_from
98 98 elif parsed.netloc and request.host != parsed.netloc:
99 99 log.error('Suspicious NETLOC detected %s for url %s server url '
100 100 'is: %s' % (parsed.netloc, parsed, request.host))
101 101 came_from = default_came_from
102 102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 103 log.error('Header injection detected `%s` for url %s server url ' %
104 104 (parsed.path, parsed))
105 105 came_from = default_came_from
106 106
107 107 return came_from or default_came_from
108 108
109 109
110 110 class LoginView(BaseAppView):
111 111
112 112 def load_default_context(self):
113 113 c = self._get_local_tmpl_context()
114 114 c.came_from = get_came_from(self.request)
115 115 self._register_global_c(c)
116 116 return c
117 117
118 118 def _get_captcha_data(self):
119 119 settings = SettingsModel().get_all_settings()
120 120 private_key = settings.get('rhodecode_captcha_private_key')
121 121 public_key = settings.get('rhodecode_captcha_public_key')
122 122 active = bool(private_key)
123 123 return CaptchaData(
124 124 active=active, private_key=private_key, public_key=public_key)
125 125
126 126 @view_config(
127 127 route_name='login', request_method='GET',
128 128 renderer='rhodecode:templates/login.mako')
129 129 def login(self):
130 130 c = self.load_default_context()
131 131 auth_user = self._rhodecode_user
132 132
133 133 # redirect if already logged in
134 134 if (auth_user.is_authenticated and
135 135 not auth_user.is_default and auth_user.ip_allowed):
136 136 raise HTTPFound(c.came_from)
137 137
138 138 # check if we use headers plugin, and try to login using it.
139 139 try:
140 140 log.debug('Running PRE-AUTH for headers based authentication')
141 141 auth_info = authenticate(
142 142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 143 if auth_info:
144 144 headers = _store_user_in_session(
145 145 self.session, auth_info.get('username'))
146 146 raise HTTPFound(c.came_from, headers=headers)
147 147 except UserCreationError as e:
148 148 log.error(e)
149 149 self.session.flash(e, queue='error')
150 150
151 151 return self._get_template_context(c)
152 152
153 153 @view_config(
154 154 route_name='login', request_method='POST',
155 155 renderer='rhodecode:templates/login.mako')
156 156 def login_post(self):
157 157 c = self.load_default_context()
158 158
159 159 login_form = LoginForm()()
160 160
161 161 try:
162 162 self.session.invalidate()
163 163 form_result = login_form.to_python(self.request.params)
164 164 # form checks for username/password, now we're authenticated
165 165 headers = _store_user_in_session(
166 166 self.session,
167 167 username=form_result['username'],
168 168 remember=form_result['remember'])
169 169 log.debug('Redirecting to "%s" after login.', c.came_from)
170 170
171 171 audit_user = audit_logger.UserWrap(
172 172 username=self.request.params.get('username'),
173 173 ip_addr=self.request.remote_addr)
174 174 action_data = {'user_agent': self.request.user_agent}
175 175 audit_logger.store_web(
176 action='user.login.success', action_data=action_data,
176 'user.login.success', action_data=action_data,
177 177 user=audit_user, commit=True)
178 178
179 179 raise HTTPFound(c.came_from, headers=headers)
180 180 except formencode.Invalid as errors:
181 181 defaults = errors.value
182 182 # remove password from filling in form again
183 183 defaults.pop('password', None)
184 184 render_ctx = self._get_template_context(c)
185 185 render_ctx.update({
186 186 'errors': errors.error_dict,
187 187 'defaults': defaults,
188 188 })
189 189
190 190 audit_user = audit_logger.UserWrap(
191 191 username=self.request.params.get('username'),
192 192 ip_addr=self.request.remote_addr)
193 193 action_data = {'user_agent': self.request.user_agent}
194 194 audit_logger.store_web(
195 action='user.login.failure', action_data=action_data,
195 'user.login.failure', action_data=action_data,
196 196 user=audit_user, commit=True)
197 197 return render_ctx
198 198
199 199 except UserCreationError as e:
200 200 # headers auth or other auth functions that create users on
201 201 # the fly can throw this exception signaling that there's issue
202 202 # with user creation, explanation should be provided in
203 203 # Exception itself
204 204 self.session.flash(e, queue='error')
205 205 return self._get_template_context(c)
206 206
207 207 @CSRFRequired()
208 208 @view_config(route_name='logout', request_method='POST')
209 209 def logout(self):
210 210 auth_user = self._rhodecode_user
211 211 log.info('Deleting session for user: `%s`', auth_user)
212 212
213 213 action_data = {'user_agent': self.request.user_agent}
214 214 audit_logger.store_web(
215 action='user.logout', action_data=action_data,
215 'user.logout', action_data=action_data,
216 216 user=auth_user, commit=True)
217 217 self.session.delete()
218 218 return HTTPFound(h.route_path('home'))
219 219
220 220 @HasPermissionAnyDecorator(
221 221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 222 @view_config(
223 223 route_name='register', request_method='GET',
224 224 renderer='rhodecode:templates/register.mako',)
225 225 def register(self, defaults=None, errors=None):
226 226 c = self.load_default_context()
227 227 defaults = defaults or {}
228 228 errors = errors or {}
229 229
230 230 settings = SettingsModel().get_all_settings()
231 231 register_message = settings.get('rhodecode_register_message') or ''
232 232 captcha = self._get_captcha_data()
233 233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 234 .AuthUser.permissions['global']
235 235
236 236 render_ctx = self._get_template_context(c)
237 237 render_ctx.update({
238 238 'defaults': defaults,
239 239 'errors': errors,
240 240 'auto_active': auto_active,
241 241 'captcha_active': captcha.active,
242 242 'captcha_public_key': captcha.public_key,
243 243 'register_message': register_message,
244 244 })
245 245 return render_ctx
246 246
247 247 @HasPermissionAnyDecorator(
248 248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 249 @view_config(
250 250 route_name='register', request_method='POST',
251 251 renderer='rhodecode:templates/register.mako')
252 252 def register_post(self):
253 253 captcha = self._get_captcha_data()
254 254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 255 .AuthUser.permissions['global']
256 256
257 257 register_form = RegisterForm()()
258 258 try:
259 259 form_result = register_form.to_python(self.request.params)
260 260 form_result['active'] = auto_active
261 261
262 262 if captcha.active:
263 263 response = submit(
264 264 self.request.params.get('recaptcha_challenge_field'),
265 265 self.request.params.get('recaptcha_response_field'),
266 266 private_key=captcha.private_key,
267 267 remoteip=get_ip_addr(self.request.environ))
268 268 if not response.is_valid:
269 269 _value = form_result
270 270 _msg = _('Bad captcha')
271 271 error_dict = {'recaptcha_field': _msg}
272 272 raise formencode.Invalid(_msg, _value, None,
273 273 error_dict=error_dict)
274 274
275 275 new_user = UserModel().create_registration(form_result)
276 276 event = UserRegistered(user=new_user, session=self.session)
277 277 self.request.registry.notify(event)
278 278 self.session.flash(
279 279 _('You have successfully registered with RhodeCode'),
280 280 queue='success')
281 281 Session().commit()
282 282
283 283 redirect_ro = self.request.route_path('login')
284 284 raise HTTPFound(redirect_ro)
285 285
286 286 except formencode.Invalid as errors:
287 287 errors.value.pop('password', None)
288 288 errors.value.pop('password_confirmation', None)
289 289 return self.register(
290 290 defaults=errors.value, errors=errors.error_dict)
291 291
292 292 except UserCreationError as e:
293 293 # container auth or other auth functions that create users on
294 294 # the fly can throw this exception signaling that there's issue
295 295 # with user creation, explanation should be provided in
296 296 # Exception itself
297 297 self.session.flash(e, queue='error')
298 298 return self.register()
299 299
300 300 @view_config(
301 301 route_name='reset_password', request_method=('GET', 'POST'),
302 302 renderer='rhodecode:templates/password_reset.mako')
303 303 def password_reset(self):
304 304 captcha = self._get_captcha_data()
305 305
306 306 render_ctx = {
307 307 'captcha_active': captcha.active,
308 308 'captcha_public_key': captcha.public_key,
309 309 'defaults': {},
310 310 'errors': {},
311 311 }
312 312
313 313 # always send implicit message to prevent from discovery of
314 314 # matching emails
315 315 msg = _('If such email exists, a password reset link was sent to it.')
316 316
317 317 if self.request.POST:
318 318 if h.HasPermissionAny('hg.password_reset.disabled')():
319 319 _email = self.request.POST.get('email', '')
320 320 log.error('Failed attempt to reset password for `%s`.', _email)
321 321 self.session.flash(_('Password reset has been disabled.'),
322 322 queue='error')
323 323 return HTTPFound(self.request.route_path('reset_password'))
324 324
325 325 password_reset_form = PasswordResetForm()()
326 326 try:
327 327 form_result = password_reset_form.to_python(
328 328 self.request.params)
329 329 user_email = form_result['email']
330 330
331 331 if captcha.active:
332 332 response = submit(
333 333 self.request.params.get('recaptcha_challenge_field'),
334 334 self.request.params.get('recaptcha_response_field'),
335 335 private_key=captcha.private_key,
336 336 remoteip=get_ip_addr(self.request.environ))
337 337 if not response.is_valid:
338 338 _value = form_result
339 339 _msg = _('Bad captcha')
340 340 error_dict = {'recaptcha_field': _msg}
341 341 raise formencode.Invalid(
342 342 _msg, _value, None, error_dict=error_dict)
343 343
344 344 # Generate reset URL and send mail.
345 345 user = User.get_by_email(user_email)
346 346
347 347 # generate password reset token that expires in 10minutes
348 348 desc = 'Generated token for password reset from {}'.format(
349 349 datetime.datetime.now().isoformat())
350 350 reset_token = AuthTokenModel().create(
351 351 user, lifetime=10,
352 352 description=desc,
353 353 role=UserApiKeys.ROLE_PASSWORD_RESET)
354 354 Session().commit()
355 355
356 356 log.debug('Successfully created password recovery token')
357 357 password_reset_url = self.request.route_url(
358 358 'reset_password_confirmation',
359 359 _query={'key': reset_token.api_key})
360 360 UserModel().reset_password_link(
361 361 form_result, password_reset_url)
362 362 # Display success message and redirect.
363 363 self.session.flash(msg, queue='success')
364 364
365 365 action_data = {'email': user_email,
366 366 'user_agent': self.request.user_agent}
367 367 audit_logger.store_web(
368 action='user.password.reset_request',
369 action_data=action_data,
368 'user.password.reset_request', action_data=action_data,
370 369 user=self._rhodecode_user, commit=True)
371 370 return HTTPFound(self.request.route_path('reset_password'))
372 371
373 372 except formencode.Invalid as errors:
374 373 render_ctx.update({
375 374 'defaults': errors.value,
376 375 'errors': errors.error_dict,
377 376 })
378 377 if not self.request.params.get('email'):
379 378 # case of empty email, we want to report that
380 379 return render_ctx
381 380
382 381 if 'recaptcha_field' in errors.error_dict:
383 382 # case of failed captcha
384 383 return render_ctx
385 384
386 385 log.debug('faking response on invalid password reset')
387 386 # make this take 2s, to prevent brute forcing.
388 387 time.sleep(2)
389 388 self.session.flash(msg, queue='success')
390 389 return HTTPFound(self.request.route_path('reset_password'))
391 390
392 391 return render_ctx
393 392
394 393 @view_config(route_name='reset_password_confirmation',
395 394 request_method='GET')
396 395 def password_reset_confirmation(self):
397 396
398 397 if self.request.GET and self.request.GET.get('key'):
399 398 # make this take 2s, to prevent brute forcing.
400 399 time.sleep(2)
401 400
402 401 token = AuthTokenModel().get_auth_token(
403 402 self.request.GET.get('key'))
404 403
405 404 # verify token is the correct role
406 405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
407 406 log.debug('Got token with role:%s expected is %s',
408 407 getattr(token, 'role', 'EMPTY_TOKEN'),
409 408 UserApiKeys.ROLE_PASSWORD_RESET)
410 409 self.session.flash(
411 410 _('Given reset token is invalid'), queue='error')
412 411 return HTTPFound(self.request.route_path('reset_password'))
413 412
414 413 try:
415 414 owner = token.user
416 415 data = {'email': owner.email, 'token': token.api_key}
417 416 UserModel().reset_password(data)
418 417 self.session.flash(
419 418 _('Your password reset was successful, '
420 419 'a new password has been sent to your email'),
421 420 queue='success')
422 421 except Exception as e:
423 422 log.error(e)
424 423 return HTTPFound(self.request.route_path('reset_password'))
425 424
426 425 return HTTPFound(self.request.route_path('login'))
@@ -1,399 +1,399 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 import formencode
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27
28 28 from rhodecode.apps._base import BaseAppView
29 29 from rhodecode import forms
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
34 34 from rhodecode.lib.channelstream import channelstream_request, \
35 35 ChannelstreamException
36 36 from rhodecode.lib.utils2 import safe_int, md5
37 37 from rhodecode.model.auth_token import AuthTokenModel
38 38 from rhodecode.model.db import (
39 39 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload)
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.scm import RepoList
42 42 from rhodecode.model.user import UserModel
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.validation_schema.schemas import user_schema
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class MyAccountView(BaseAppView):
50 50 ALLOW_SCOPED_TOKENS = False
51 51 """
52 52 This view has alternative version inside EE, if modified please take a look
53 53 in there as well.
54 54 """
55 55
56 56 def load_default_context(self):
57 57 c = self._get_local_tmpl_context()
58 58 c.user = c.auth_user.get_instance()
59 59 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
60 60 self._register_global_c(c)
61 61 return c
62 62
63 63 @LoginRequired()
64 64 @NotAnonymous()
65 65 @view_config(
66 66 route_name='my_account_profile', request_method='GET',
67 67 renderer='rhodecode:templates/admin/my_account/my_account.mako')
68 68 def my_account_profile(self):
69 69 c = self.load_default_context()
70 70 c.active = 'profile'
71 71 return self._get_template_context(c)
72 72
73 73 @LoginRequired()
74 74 @NotAnonymous()
75 75 @view_config(
76 76 route_name='my_account_password', request_method='GET',
77 77 renderer='rhodecode:templates/admin/my_account/my_account.mako')
78 78 def my_account_password(self):
79 79 c = self.load_default_context()
80 80 c.active = 'password'
81 81 c.extern_type = c.user.extern_type
82 82
83 83 schema = user_schema.ChangePasswordSchema().bind(
84 84 username=c.user.username)
85 85
86 86 form = forms.Form(
87 87 schema, buttons=(forms.buttons.save, forms.buttons.reset))
88 88
89 89 c.form = form
90 90 return self._get_template_context(c)
91 91
92 92 @LoginRequired()
93 93 @NotAnonymous()
94 94 @CSRFRequired()
95 95 @view_config(
96 96 route_name='my_account_password', request_method='POST',
97 97 renderer='rhodecode:templates/admin/my_account/my_account.mako')
98 98 def my_account_password_update(self):
99 99 _ = self.request.translate
100 100 c = self.load_default_context()
101 101 c.active = 'password'
102 102 c.extern_type = c.user.extern_type
103 103
104 104 schema = user_schema.ChangePasswordSchema().bind(
105 105 username=c.user.username)
106 106
107 107 form = forms.Form(
108 108 schema, buttons=(forms.buttons.save, forms.buttons.reset))
109 109
110 110 if c.extern_type != 'rhodecode':
111 111 raise HTTPFound(self.request.route_path('my_account_password'))
112 112
113 113 controls = self.request.POST.items()
114 114 try:
115 115 valid_data = form.validate(controls)
116 116 UserModel().update_user(c.user.user_id, **valid_data)
117 117 c.user.update_userdata(force_password_change=False)
118 118 Session().commit()
119 119 except forms.ValidationFailure as e:
120 120 c.form = e
121 121 return self._get_template_context(c)
122 122
123 123 except Exception:
124 124 log.exception("Exception updating password")
125 125 h.flash(_('Error occurred during update of user password'),
126 126 category='error')
127 127 else:
128 128 instance = c.auth_user.get_instance()
129 129 self.session.setdefault('rhodecode_user', {}).update(
130 130 {'password': md5(instance.password)})
131 131 self.session.save()
132 132 h.flash(_("Successfully updated password"), category='success')
133 133
134 134 raise HTTPFound(self.request.route_path('my_account_password'))
135 135
136 136 @LoginRequired()
137 137 @NotAnonymous()
138 138 @view_config(
139 139 route_name='my_account_auth_tokens', request_method='GET',
140 140 renderer='rhodecode:templates/admin/my_account/my_account.mako')
141 141 def my_account_auth_tokens(self):
142 142 _ = self.request.translate
143 143
144 144 c = self.load_default_context()
145 145 c.active = 'auth_tokens'
146 146
147 147 c.lifetime_values = [
148 148 (str(-1), _('forever')),
149 149 (str(5), _('5 minutes')),
150 150 (str(60), _('1 hour')),
151 151 (str(60 * 24), _('1 day')),
152 152 (str(60 * 24 * 30), _('1 month')),
153 153 ]
154 154 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
155 155 c.role_values = [
156 156 (x, AuthTokenModel.cls._get_role_name(x))
157 157 for x in AuthTokenModel.cls.ROLES]
158 158 c.role_options = [(c.role_values, _("Role"))]
159 159 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
160 160 c.user.user_id, show_expired=True)
161 161 return self._get_template_context(c)
162 162
163 163 def maybe_attach_token_scope(self, token):
164 164 # implemented in EE edition
165 165 pass
166 166
167 167 @LoginRequired()
168 168 @NotAnonymous()
169 169 @CSRFRequired()
170 170 @view_config(
171 171 route_name='my_account_auth_tokens_add', request_method='POST',)
172 172 def my_account_auth_tokens_add(self):
173 173 _ = self.request.translate
174 174 c = self.load_default_context()
175 175
176 176 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
177 177 description = self.request.POST.get('description')
178 178 role = self.request.POST.get('role')
179 179
180 180 token = AuthTokenModel().create(
181 181 c.user.user_id, description, lifetime, role)
182 182 token_data = token.get_api_data()
183 183
184 184 self.maybe_attach_token_scope(token)
185 185 audit_logger.store_web(
186 action='user.edit.token.add',
187 action_data={'data': {'token': token_data, 'user': 'self'}},
186 'user.edit.token.add', action_data={
187 'data': {'token': token_data, 'user': 'self'}},
188 188 user=self._rhodecode_user, )
189 189 Session().commit()
190 190
191 191 h.flash(_("Auth token successfully created"), category='success')
192 192 return HTTPFound(h.route_path('my_account_auth_tokens'))
193 193
194 194 @LoginRequired()
195 195 @NotAnonymous()
196 196 @CSRFRequired()
197 197 @view_config(
198 198 route_name='my_account_auth_tokens_delete', request_method='POST')
199 199 def my_account_auth_tokens_delete(self):
200 200 _ = self.request.translate
201 201 c = self.load_default_context()
202 202
203 203 del_auth_token = self.request.POST.get('del_auth_token')
204 204
205 205 if del_auth_token:
206 206 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
207 207 token_data = token.get_api_data()
208 208
209 209 AuthTokenModel().delete(del_auth_token, c.user.user_id)
210 210 audit_logger.store_web(
211 action='user.edit.token.delete',
212 action_data={'data': {'token': token_data, 'user': 'self'}},
211 'user.edit.token.delete', action_data={
212 'data': {'token': token_data, 'user': 'self'}},
213 213 user=self._rhodecode_user,)
214 214 Session().commit()
215 215 h.flash(_("Auth token successfully deleted"), category='success')
216 216
217 217 return HTTPFound(h.route_path('my_account_auth_tokens'))
218 218
219 219 @LoginRequired()
220 220 @NotAnonymous()
221 221 @view_config(
222 222 route_name='my_account_emails', request_method='GET',
223 223 renderer='rhodecode:templates/admin/my_account/my_account.mako')
224 224 def my_account_emails(self):
225 225 _ = self.request.translate
226 226
227 227 c = self.load_default_context()
228 228 c.active = 'emails'
229 229
230 230 c.user_email_map = UserEmailMap.query()\
231 231 .filter(UserEmailMap.user == c.user).all()
232 232 return self._get_template_context(c)
233 233
234 234 @LoginRequired()
235 235 @NotAnonymous()
236 236 @CSRFRequired()
237 237 @view_config(
238 238 route_name='my_account_emails_add', request_method='POST')
239 239 def my_account_emails_add(self):
240 240 _ = self.request.translate
241 241 c = self.load_default_context()
242 242
243 243 email = self.request.POST.get('new_email')
244 244
245 245 try:
246 246 UserModel().add_extra_email(c.user.user_id, email)
247 247 audit_logger.store_web(
248 action='user.edit.email.add',
249 action_data={'data': {'email': email, 'user': 'self'}},
248 'user.edit.email.add', action_data={
249 'data': {'email': email, 'user': 'self'}},
250 250 user=self._rhodecode_user,)
251 251
252 252 Session().commit()
253 253 h.flash(_("Added new email address `%s` for user account") % email,
254 254 category='success')
255 255 except formencode.Invalid as error:
256 256 h.flash(h.escape(error.error_dict['email']), category='error')
257 257 except Exception:
258 258 log.exception("Exception in my_account_emails")
259 259 h.flash(_('An error occurred during email saving'),
260 260 category='error')
261 261 return HTTPFound(h.route_path('my_account_emails'))
262 262
263 263 @LoginRequired()
264 264 @NotAnonymous()
265 265 @CSRFRequired()
266 266 @view_config(
267 267 route_name='my_account_emails_delete', request_method='POST')
268 268 def my_account_emails_delete(self):
269 269 _ = self.request.translate
270 270 c = self.load_default_context()
271 271
272 272 del_email_id = self.request.POST.get('del_email_id')
273 273 if del_email_id:
274 274 email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
275 275 UserModel().delete_extra_email(c.user.user_id, del_email_id)
276 276 audit_logger.store_web(
277 action='user.edit.email.delete',
278 action_data={'data': {'email': email, 'user': 'self'}},
277 'user.edit.email.delete', action_data={
278 'data': {'email': email, 'user': 'self'}},
279 279 user=self._rhodecode_user,)
280 280 Session().commit()
281 281 h.flash(_("Email successfully deleted"),
282 282 category='success')
283 283 return HTTPFound(h.route_path('my_account_emails'))
284 284
285 285 @LoginRequired()
286 286 @NotAnonymous()
287 287 @CSRFRequired()
288 288 @view_config(
289 289 route_name='my_account_notifications_test_channelstream',
290 290 request_method='POST', renderer='json_ext')
291 291 def my_account_notifications_test_channelstream(self):
292 292 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
293 293 self._rhodecode_user.username, datetime.datetime.now())
294 294 payload = {
295 295 # 'channel': 'broadcast',
296 296 'type': 'message',
297 297 'timestamp': datetime.datetime.utcnow(),
298 298 'user': 'system',
299 299 'pm_users': [self._rhodecode_user.username],
300 300 'message': {
301 301 'message': message,
302 302 'level': 'info',
303 303 'topic': '/notifications'
304 304 }
305 305 }
306 306
307 307 registry = self.request.registry
308 308 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
309 309 channelstream_config = rhodecode_plugins.get('channelstream', {})
310 310
311 311 try:
312 312 channelstream_request(channelstream_config, [payload], '/message')
313 313 except ChannelstreamException as e:
314 314 log.exception('Failed to send channelstream data')
315 315 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
316 316 return {"response": 'Channelstream data sent. '
317 317 'You should see a new live message now.'}
318 318
319 319 def _load_my_repos_data(self, watched=False):
320 320 if watched:
321 321 admin = False
322 322 follows_repos = Session().query(UserFollowing)\
323 323 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
324 324 .options(joinedload(UserFollowing.follows_repository))\
325 325 .all()
326 326 repo_list = [x.follows_repository for x in follows_repos]
327 327 else:
328 328 admin = True
329 329 repo_list = Repository.get_all_repos(
330 330 user_id=self._rhodecode_user.user_id)
331 331 repo_list = RepoList(repo_list, perm_set=[
332 332 'repository.read', 'repository.write', 'repository.admin'])
333 333
334 334 repos_data = RepoModel().get_repos_as_dict(
335 335 repo_list=repo_list, admin=admin)
336 336 # json used to render the grid
337 337 return json.dumps(repos_data)
338 338
339 339 @LoginRequired()
340 340 @NotAnonymous()
341 341 @view_config(
342 342 route_name='my_account_repos', request_method='GET',
343 343 renderer='rhodecode:templates/admin/my_account/my_account.mako')
344 344 def my_account_repos(self):
345 345 c = self.load_default_context()
346 346 c.active = 'repos'
347 347
348 348 # json used to render the grid
349 349 c.data = self._load_my_repos_data()
350 350 return self._get_template_context(c)
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @view_config(
355 355 route_name='my_account_watched', request_method='GET',
356 356 renderer='rhodecode:templates/admin/my_account/my_account.mako')
357 357 def my_account_watched(self):
358 358 c = self.load_default_context()
359 359 c.active = 'watched'
360 360
361 361 # json used to render the grid
362 362 c.data = self._load_my_repos_data(watched=True)
363 363 return self._get_template_context(c)
364 364
365 365 @LoginRequired()
366 366 @NotAnonymous()
367 367 @view_config(
368 368 route_name='my_account_perms', request_method='GET',
369 369 renderer='rhodecode:templates/admin/my_account/my_account.mako')
370 370 def my_account_perms(self):
371 371 c = self.load_default_context()
372 372 c.active = 'perms'
373 373
374 374 c.perm_user = c.auth_user
375 375 return self._get_template_context(c)
376 376
377 377 @LoginRequired()
378 378 @NotAnonymous()
379 379 @view_config(
380 380 route_name='my_account_notifications', request_method='GET',
381 381 renderer='rhodecode:templates/admin/my_account/my_account.mako')
382 382 def my_notifications(self):
383 383 c = self.load_default_context()
384 384 c.active = 'notifications'
385 385
386 386 return self._get_template_context(c)
387 387
388 388 @LoginRequired()
389 389 @NotAnonymous()
390 390 @CSRFRequired()
391 391 @view_config(
392 392 route_name='my_account_notifications_toggle_visibility',
393 393 request_method='POST', renderer='json_ext')
394 394 def my_notifications_toggle_visibility(self):
395 395 user = self._rhodecode_db_user
396 396 new_status = not user.user_data.get('notification_status', True)
397 397 user.update_userdata(notification_status=new_status)
398 398 Session().commit()
399 399 return user.user_data['notification_status'] No newline at end of file
@@ -1,227 +1,226 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.view import view_config
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.apps._base import RepoAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
31 31 from rhodecode.lib.exceptions import AttachedForksError
32 32 from rhodecode.lib.utils2 import safe_int
33 33 from rhodecode.lib.vcs import RepositoryError
34 34 from rhodecode.model.db import Session, UserFollowing, User, Repository
35 35 from rhodecode.model.repo import RepoModel
36 36 from rhodecode.model.scm import ScmModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class RepoSettingsView(RepoAppView):
42 42
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 45
46 46 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
47 47 c.repo_info = self.db_repo
48 48
49 49 self._register_global_c(c)
50 50 return c
51 51
52 52 @LoginRequired()
53 53 @HasRepoPermissionAnyDecorator('repository.admin')
54 54 @view_config(
55 55 route_name='edit_repo_advanced', request_method='GET',
56 56 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
57 57 def edit_advanced(self):
58 58 c = self.load_default_context()
59 59 c.active = 'advanced'
60 60
61 61 c.default_user_id = User.get_default_user().user_id
62 62 c.in_public_journal = UserFollowing.query() \
63 63 .filter(UserFollowing.user_id == c.default_user_id) \
64 64 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
65 65
66 66 c.has_origin_repo_read_perm = False
67 67 if self.db_repo.fork:
68 68 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
69 69 'repository.write', 'repository.read', 'repository.admin')(
70 70 self.db_repo.fork.repo_name, 'repo set as fork page')
71 71
72 72 return self._get_template_context(c)
73 73
74 74 @LoginRequired()
75 75 @HasRepoPermissionAnyDecorator('repository.admin')
76 76 @CSRFRequired()
77 77 @view_config(
78 78 route_name='edit_repo_advanced_delete', request_method='POST',
79 79 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
80 80 def edit_advanced_delete(self):
81 81 """
82 82 Deletes the repository, or shows warnings if deletion is not possible
83 83 because of attached forks or other errors.
84 84 """
85 85 _ = self.request.translate
86 86 handle_forks = self.request.POST.get('forks', None)
87 87
88 88 try:
89 89 _forks = self.db_repo.forks.count()
90 90 if _forks and handle_forks:
91 91 if handle_forks == 'detach_forks':
92 92 handle_forks = 'detach'
93 93 h.flash(_('Detached %s forks') % _forks, category='success')
94 94 elif handle_forks == 'delete_forks':
95 95 handle_forks = 'delete'
96 96 h.flash(_('Deleted %s forks') % _forks, category='success')
97 97
98 repo_data = self.db_repo.get_api_data()
98 old_data = self.db_repo.get_api_data()
99 99 RepoModel().delete(self.db_repo, forks=handle_forks)
100 100
101 101 repo = audit_logger.RepoWrap(repo_id=None,
102 102 repo_name=self.db_repo.repo_name)
103 103 audit_logger.store_web(
104 action='repo.delete',
105 action_data={'data': repo_data},
104 'repo.delete', action_data={'old_data': old_data},
106 105 user=self._rhodecode_user, repo=repo)
107 106
108 107 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
109 108 h.flash(
110 109 _('Deleted repository `%s`') % self.db_repo_name,
111 110 category='success')
112 111 Session().commit()
113 112 except AttachedForksError:
114 113 repo_advanced_url = h.route_path(
115 114 'edit_repo_advanced', repo_name=self.db_repo_name,
116 115 _anchor='advanced-delete')
117 116 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
118 117 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
119 118 'Try using {delete_or_detach} option.')
120 119 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
121 120 category='warning')
122 121
123 122 # redirect to advanced for forks handle action ?
124 123 raise HTTPFound(repo_advanced_url)
125 124
126 125 except Exception:
127 126 log.exception("Exception during deletion of repository")
128 127 h.flash(_('An error occurred during deletion of `%s`')
129 128 % self.db_repo_name, category='error')
130 129 # redirect to advanced for more deletion options
131 130 raise HTTPFound(
132 131 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name),
133 132 _anchor='advanced-delete')
134 133
135 134 raise HTTPFound(h.route_path('home'))
136 135
137 136 @LoginRequired()
138 137 @HasRepoPermissionAnyDecorator('repository.admin')
139 138 @CSRFRequired()
140 139 @view_config(
141 140 route_name='edit_repo_advanced_journal', request_method='POST',
142 141 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
143 142 def edit_advanced_journal(self):
144 143 """
145 144 Set's this repository to be visible in public journal,
146 145 in other words making default user to follow this repo
147 146 """
148 147 _ = self.request.translate
149 148
150 149 try:
151 150 user_id = User.get_default_user().user_id
152 151 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
153 152 h.flash(_('Updated repository visibility in public journal'),
154 153 category='success')
155 154 Session().commit()
156 155 except Exception:
157 156 h.flash(_('An error occurred during setting this '
158 157 'repository in public journal'),
159 158 category='error')
160 159
161 160 raise HTTPFound(
162 161 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
163 162
164 163 @LoginRequired()
165 164 @HasRepoPermissionAnyDecorator('repository.admin')
166 165 @CSRFRequired()
167 166 @view_config(
168 167 route_name='edit_repo_advanced_fork', request_method='POST',
169 168 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
170 169 def edit_advanced_fork(self):
171 170 """
172 171 Mark given repository as a fork of another
173 172 """
174 173 _ = self.request.translate
175 174
176 175 new_fork_id = self.request.POST.get('id_fork_of')
177 176 try:
178 177
179 178 if new_fork_id and not new_fork_id.isdigit():
180 179 log.error('Given fork id %s is not an INT', new_fork_id)
181 180
182 181 fork_id = safe_int(new_fork_id)
183 182 repo = ScmModel().mark_as_fork(
184 183 self.db_repo_name, fork_id, self._rhodecode_user.user_id)
185 184 fork = repo.fork.repo_name if repo.fork else _('Nothing')
186 185 Session().commit()
187 186 h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
188 187 category='success')
189 188 except RepositoryError as e:
190 189 log.exception("Repository Error occurred")
191 190 h.flash(str(e), category='error')
192 191 except Exception as e:
193 192 log.exception("Exception while editing fork")
194 193 h.flash(_('An error occurred during this operation'),
195 194 category='error')
196 195
197 196 raise HTTPFound(
198 197 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
199 198
200 199 @LoginRequired()
201 200 @HasRepoPermissionAnyDecorator('repository.admin')
202 201 @CSRFRequired()
203 202 @view_config(
204 203 route_name='edit_repo_advanced_locking', request_method='POST',
205 204 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
206 205 def edit_advanced_locking(self):
207 206 """
208 207 Toggle locking of repository
209 208 """
210 209 _ = self.request.translate
211 210 set_lock = self.request.POST.get('set_lock')
212 211 set_unlock = self.request.POST.get('set_unlock')
213 212
214 213 try:
215 214 if set_lock:
216 215 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
217 216 lock_reason=Repository.LOCK_WEB)
218 217 h.flash(_('Locked repository'), category='success')
219 218 elif set_unlock:
220 219 Repository.unlock(self.db_repo)
221 220 h.flash(_('Unlocked repository'), category='success')
222 221 except Exception as e:
223 222 log.exception("Exception during unlocking")
224 223 h.flash(_('An error occurred during unlocking'), category='error')
225 224
226 225 raise HTTPFound(
227 226 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,118 +1,116 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 from pyramid.view import view_config
23 23
24 24 from rhodecode.apps._base import RepoAppView
25 25 from rhodecode.lib import audit_logger
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
28 28 NotAnonymous, CSRFRequired)
29 29 from rhodecode.lib.ext_json import json
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class StripView(RepoAppView):
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37
38 38 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
39 39 c.repo_info = self.db_repo
40 40
41 41 self._register_global_c(c)
42 42 return c
43 43
44 44 @LoginRequired()
45 45 @HasRepoPermissionAnyDecorator('repository.admin')
46 46 @view_config(
47 47 route_name='strip', request_method='GET',
48 48 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
49 49 def strip(self):
50 50 c = self.load_default_context()
51 51 c.active = 'strip'
52 52 c.strip_limit = 10
53 53
54 54 return self._get_template_context(c)
55 55
56 56 @LoginRequired()
57 57 @HasRepoPermissionAnyDecorator('repository.admin')
58 58 @CSRFRequired()
59 59 @view_config(
60 60 route_name='strip_check', request_method='POST',
61 61 renderer='json', xhr=True)
62 62 def strip_check(self):
63 63 from rhodecode.lib.vcs.backends.base import EmptyCommit
64 64 data = {}
65 65 rp = self.request.POST
66 66 for i in range(1, 11):
67 67 chset = 'changeset_id-%d' % (i,)
68 68 check = rp.get(chset)
69 69
70 70 if check:
71 71 data[i] = self.db_repo.get_changeset(rp[chset])
72 72 if isinstance(data[i], EmptyCommit):
73 73 data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
74 74 else:
75 75 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
76 76 'author': data[i].author,
77 77 'comment': data[i].message}
78 78 else:
79 79 break
80 80 return data
81 81
82 82 @LoginRequired()
83 83 @HasRepoPermissionAnyDecorator('repository.admin')
84 84 @CSRFRequired()
85 85 @view_config(
86 86 route_name='strip_execute', request_method='POST',
87 87 renderer='json', xhr=True)
88 88 def strip_execute(self):
89 89 from rhodecode.model.scm import ScmModel
90 90
91 91 c = self.load_default_context()
92 92 user = self._rhodecode_user
93 93 rp = self.request.POST
94 94 data = {}
95 95 for idx in rp:
96 96 commit = json.loads(rp[idx])
97 97 # If someone put two times the same branch
98 98 if commit['branch'] in data.keys():
99 99 continue
100 100 try:
101 101 ScmModel().strip(
102 102 repo=c.repo_info,
103 103 commit_id=commit['rev'], branch=commit['branch'])
104 104 log.info('Stripped commit %s from repo `%s` by %s' % (
105 105 commit['rev'], c.repo_info.repo_name, user))
106 106 data[commit['rev']] = True
107 107
108 108 audit_logger.store_web(
109 action='repo.commit.strip',
110 action_data={'commit_id': commit['rev']},
111 repo=self.db_repo,
112 user=self._rhodecode_user, commit=True)
109 'repo.commit.strip', action_data={'commit_id': commit['rev']},
110 repo=self.db_repo, user=self._rhodecode_user, commit=True)
113 111
114 112 except Exception as e:
115 113 data[commit['rev']] = False
116 114 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (
117 115 commit['rev'], self.db_repo_name, user, e.message))
118 116 return data
@@ -1,404 +1,405 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Repository groups controller for RhodeCode
24 24 """
25 25
26 26 import logging
27 27 import formencode
28 28
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _, ungettext
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, NotAnonymous, HasPermissionAll,
41 41 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
42 42 from rhodecode.lib.base import BaseController, render
43 43 from rhodecode.lib.utils2 import safe_int
44 44 from rhodecode.model.db import RepoGroup, User
45 45 from rhodecode.model.scm import RepoGroupList
46 46 from rhodecode.model.repo_group import RepoGroupModel
47 47 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
48 48 from rhodecode.model.meta import Session
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class RepoGroupsController(BaseController):
55 55 """REST Controller styled on the Atom Publishing Protocol"""
56 56
57 57 @LoginRequired()
58 58 def __before__(self):
59 59 super(RepoGroupsController, self).__before__()
60 60
61 61 def __load_defaults(self, allow_empty_group=False, repo_group=None):
62 62 if self._can_create_repo_group():
63 63 # we're global admin, we're ok and we can create TOP level groups
64 64 allow_empty_group = True
65 65
66 66 # override the choices for this form, we need to filter choices
67 67 # and display only those we have ADMIN right
68 68 groups_with_admin_rights = RepoGroupList(
69 69 RepoGroup.query().all(),
70 70 perm_set=['group.admin'])
71 71 c.repo_groups = RepoGroup.groups_choices(
72 72 groups=groups_with_admin_rights,
73 73 show_empty_group=allow_empty_group)
74 74
75 75 if repo_group:
76 76 # exclude filtered ids
77 77 exclude_group_ids = [repo_group.group_id]
78 78 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
79 79 c.repo_groups)
80 80 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
81 81 parent_group = repo_group.parent_group
82 82
83 83 add_parent_group = (parent_group and (
84 84 unicode(parent_group.group_id) not in c.repo_groups_choices))
85 85 if add_parent_group:
86 86 c.repo_groups_choices.append(unicode(parent_group.group_id))
87 87 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
88 88
89 89 def __load_data(self, group_id):
90 90 """
91 91 Load defaults settings for edit, and update
92 92
93 93 :param group_id:
94 94 """
95 95 repo_group = RepoGroup.get_or_404(group_id)
96 96 data = repo_group.get_dict()
97 97 data['group_name'] = repo_group.name
98 98
99 99 # fill owner
100 100 if repo_group.user:
101 101 data.update({'user': repo_group.user.username})
102 102 else:
103 103 replacement_user = User.get_first_super_admin().username
104 104 data.update({'user': replacement_user})
105 105
106 106 # fill repository group users
107 107 for p in repo_group.repo_group_to_perm:
108 108 data.update({
109 109 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
110 110
111 111 # fill repository group user groups
112 112 for p in repo_group.users_group_to_perm:
113 113 data.update({
114 114 'g_perm_%s' % p.users_group.users_group_id:
115 115 p.permission.permission_name})
116 116 # html and form expects -1 as empty parent group
117 117 data['group_parent_id'] = data['group_parent_id'] or -1
118 118 return data
119 119
120 120 def _revoke_perms_on_yourself(self, form_result):
121 121 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
122 122 form_result['perm_updates'])
123 123 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
124 124 form_result['perm_additions'])
125 125 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
126 126 form_result['perm_deletions'])
127 127 admin_perm = 'group.admin'
128 128 if _updates and _updates[0][1] != admin_perm or \
129 129 _additions and _additions[0][1] != admin_perm or \
130 130 _deletions and _deletions[0][1] != admin_perm:
131 131 return True
132 132 return False
133 133
134 134 def _can_create_repo_group(self, parent_group_id=None):
135 135 is_admin = HasPermissionAll('hg.admin')('group create controller')
136 136 create_repo_group = HasPermissionAll(
137 137 'hg.repogroup.create.true')('group create controller')
138 138 if is_admin or (create_repo_group and not parent_group_id):
139 139 # we're global admin, or we have global repo group create
140 140 # permission
141 141 # we're ok and we can create TOP level groups
142 142 return True
143 143 elif parent_group_id:
144 144 # we check the permission if we can write to parent group
145 145 group = RepoGroup.get(parent_group_id)
146 146 group_name = group.group_name if group else None
147 147 if HasRepoGroupPermissionAll('group.admin')(
148 148 group_name, 'check if user is an admin of group'):
149 149 # we're an admin of passed in group, we're ok.
150 150 return True
151 151 else:
152 152 return False
153 153 return False
154 154
155 155 @NotAnonymous()
156 156 def index(self):
157 157 repo_group_list = RepoGroup.get_all_repo_groups()
158 158 _perms = ['group.admin']
159 159 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
160 160 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
161 161 repo_group_list=repo_group_list_acl, admin=True)
162 162 c.data = json.dumps(repo_group_data)
163 163 return render('admin/repo_groups/repo_groups.mako')
164 164
165 165 # perm checks inside
166 166 @NotAnonymous()
167 167 @auth.CSRFRequired()
168 168 def create(self):
169 169
170 170 parent_group_id = safe_int(request.POST.get('group_parent_id'))
171 171 can_create = self._can_create_repo_group(parent_group_id)
172 172
173 173 self.__load_defaults()
174 174 # permissions for can create group based on parent_id are checked
175 175 # here in the Form
176 176 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
177 177 repo_group_form = RepoGroupForm(available_groups=available_groups,
178 178 can_create_in_root=can_create)()
179 179 try:
180 180 owner = c.rhodecode_user
181 181 form_result = repo_group_form.to_python(dict(request.POST))
182 182 repo_group = RepoGroupModel().create(
183 183 group_name=form_result['group_name_full'],
184 184 group_description=form_result['group_description'],
185 185 owner=owner.user_id,
186 186 copy_permissions=form_result['group_copy_permissions']
187 187 )
188 Session().commit()
189 repo_group_data = repo_group.get_api_data()
190 _new_group_name = form_result['group_name_full']
188 Session().flush()
191 189
190 repo_group_data = repo_group.get_api_data()
192 191 audit_logger.store_web(
193 action='repo_group.create',
194 action_data={'data': repo_group_data},
195 user=c.rhodecode_user, commit=True)
192 'repo_group.create', action_data={'data': repo_group_data},
193 user=c.rhodecode_user)
194
195 Session().commit()
196
197 _new_group_name = form_result['group_name_full']
196 198
197 199 repo_group_url = h.link_to(
198 200 _new_group_name,
199 201 h.route_path('repo_group_home', repo_group_name=_new_group_name))
200 202 h.flash(h.literal(_('Created repository group %s')
201 203 % repo_group_url), category='success')
202 204
203 205 except formencode.Invalid as errors:
204 206 return htmlfill.render(
205 207 render('admin/repo_groups/repo_group_add.mako'),
206 208 defaults=errors.value,
207 209 errors=errors.error_dict or {},
208 210 prefix_error=False,
209 211 encoding="UTF-8",
210 212 force_defaults=False)
211 213 except Exception:
212 214 log.exception("Exception during creation of repository group")
213 215 h.flash(_('Error occurred during creation of repository group %s')
214 216 % request.POST.get('group_name'), category='error')
215 217
216 218 # TODO: maybe we should get back to the main view, not the admin one
217 219 return redirect(url('repo_groups', parent_group=parent_group_id))
218 220
219 221 # perm checks inside
220 222 @NotAnonymous()
221 223 def new(self):
222 224 # perm check for admin, create_group perm or admin of parent_group
223 225 parent_group_id = safe_int(request.GET.get('parent_group'))
224 226 if not self._can_create_repo_group(parent_group_id):
225 227 return abort(403)
226 228
227 229 self.__load_defaults()
228 230 return render('admin/repo_groups/repo_group_add.mako')
229 231
230 232 @HasRepoGroupPermissionAnyDecorator('group.admin')
231 233 @auth.CSRFRequired()
232 234 def update(self, group_name):
233 235
234 236 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
235 237 can_create_in_root = self._can_create_repo_group()
236 238 show_root_location = can_create_in_root
237 239 if not c.repo_group.parent_group:
238 240 # this group don't have a parrent so we should show empty value
239 241 show_root_location = True
240 242 self.__load_defaults(allow_empty_group=show_root_location,
241 243 repo_group=c.repo_group)
242 244
243 245 repo_group_form = RepoGroupForm(
244 246 edit=True, old_data=c.repo_group.get_dict(),
245 247 available_groups=c.repo_groups_choices,
246 248 can_create_in_root=can_create_in_root, allow_disabled=True)()
247 249
248 250 old_values = c.repo_group.get_api_data()
249 251 try:
250 252 form_result = repo_group_form.to_python(dict(request.POST))
251 253 gr_name = form_result['group_name']
252 254 new_gr = RepoGroupModel().update(group_name, form_result)
253 255
254 256 audit_logger.store_web(
255 257 'repo_group.edit', action_data={'old_data': old_values},
256 258 user=c.rhodecode_user)
257 259
258 260 Session().commit()
259 261 h.flash(_('Updated repository group %s') % (gr_name,),
260 262 category='success')
261 263 # we now have new name !
262 264 group_name = new_gr.group_name
263 265 except formencode.Invalid as errors:
264 266 c.active = 'settings'
265 267 return htmlfill.render(
266 268 render('admin/repo_groups/repo_group_edit.mako'),
267 269 defaults=errors.value,
268 270 errors=errors.error_dict or {},
269 271 prefix_error=False,
270 272 encoding="UTF-8",
271 273 force_defaults=False)
272 274 except Exception:
273 275 log.exception("Exception during update or repository group")
274 276 h.flash(_('Error occurred during update of repository group %s')
275 277 % request.POST.get('group_name'), category='error')
276 278
277 279 return redirect(url('edit_repo_group', group_name=group_name))
278 280
279 281 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 282 @auth.CSRFRequired()
281 283 def delete(self, group_name):
282 284 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
283 285 repos = gr.repositories.all()
284 286 if repos:
285 287 msg = ungettext(
286 288 'This group contains %(num)d repository and cannot be deleted',
287 289 'This group contains %(num)d repositories and cannot be'
288 290 ' deleted',
289 291 len(repos)) % {'num': len(repos)}
290 292 h.flash(msg, category='warning')
291 293 return redirect(url('repo_groups'))
292 294
293 295 children = gr.children.all()
294 296 if children:
295 297 msg = ungettext(
296 298 'This group contains %(num)d subgroup and cannot be deleted',
297 299 'This group contains %(num)d subgroups and cannot be deleted',
298 300 len(children)) % {'num': len(children)}
299 301 h.flash(msg, category='warning')
300 302 return redirect(url('repo_groups'))
301 303
302 304 try:
303 305 old_values = gr.get_api_data()
304 306 RepoGroupModel().delete(group_name)
305 307
306 308 audit_logger.store_web(
307 'repo_group.delete',
308 action_data={'old_data': old_values},
309 'repo_group.delete', action_data={'old_data': old_values},
309 310 user=c.rhodecode_user)
310 311
311 312 Session().commit()
312 313 h.flash(_('Removed repository group %s') % group_name,
313 314 category='success')
314 315 except Exception:
315 316 log.exception("Exception during deletion of repository group")
316 317 h.flash(_('Error occurred during deletion of repository group %s')
317 318 % group_name, category='error')
318 319
319 320 return redirect(url('repo_groups'))
320 321
321 322 @HasRepoGroupPermissionAnyDecorator('group.admin')
322 323 def edit(self, group_name):
323 324
324 325 c.active = 'settings'
325 326
326 327 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
327 328 # we can only allow moving empty group if it's already a top-level
328 329 # group, ie has no parents, or we're admin
329 330 can_create_in_root = self._can_create_repo_group()
330 331 show_root_location = can_create_in_root
331 332 if not c.repo_group.parent_group:
332 333 # this group don't have a parrent so we should show empty value
333 334 show_root_location = True
334 335 self.__load_defaults(allow_empty_group=show_root_location,
335 336 repo_group=c.repo_group)
336 337 defaults = self.__load_data(c.repo_group.group_id)
337 338
338 339 return htmlfill.render(
339 340 render('admin/repo_groups/repo_group_edit.mako'),
340 341 defaults=defaults,
341 342 encoding="UTF-8",
342 343 force_defaults=False
343 344 )
344 345
345 346 @HasRepoGroupPermissionAnyDecorator('group.admin')
346 347 def edit_repo_group_advanced(self, group_name):
347 348 c.active = 'advanced'
348 349 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
349 350
350 351 return render('admin/repo_groups/repo_group_edit.mako')
351 352
352 353 @HasRepoGroupPermissionAnyDecorator('group.admin')
353 354 def edit_repo_group_perms(self, group_name):
354 355 c.active = 'perms'
355 356 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
356 357 self.__load_defaults()
357 358 defaults = self.__load_data(c.repo_group.group_id)
358 359
359 360 return htmlfill.render(
360 361 render('admin/repo_groups/repo_group_edit.mako'),
361 362 defaults=defaults,
362 363 encoding="UTF-8",
363 364 force_defaults=False
364 365 )
365 366
366 367 @HasRepoGroupPermissionAnyDecorator('group.admin')
367 368 @auth.CSRFRequired()
368 369 def update_perms(self, group_name):
369 370 """
370 371 Update permissions for given repository group
371 372 """
372 373
373 374 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
374 375 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
375 376 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
376 377 request.POST)
377 378
378 379 if not c.rhodecode_user.is_admin:
379 380 if self._revoke_perms_on_yourself(form):
380 381 msg = _('Cannot change permission for yourself as admin')
381 382 h.flash(msg, category='warning')
382 383 return redirect(
383 384 url('edit_repo_group_perms', group_name=group_name))
384 385
385 386 # iterate over all members(if in recursive mode) of this groups and
386 387 # set the permissions !
387 388 # this can be potentially heavy operation
388 389 changes = RepoGroupModel().update_permissions(
389 390 c.repo_group,
390 391 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
391 392 form['recursive'])
392 393
393 394 action_data = {
394 395 'added': changes['added'],
395 396 'updated': changes['updated'],
396 397 'deleted': changes['deleted'],
397 398 }
398 399 audit_logger.store_web(
399 400 'repo_group.edit.permissions', action_data=action_data,
400 401 user=c.rhodecode_user)
401 402
402 403 Session().commit()
403 404 h.flash(_('Repository Group permissions updated'), category='success')
404 405 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -1,510 +1,513 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 User Groups crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 import peppercorn
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, config
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from sqlalchemy.orm import joinedload
35 35
36 36 from rhodecode.lib import auth
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.ext_json import json
40 40 from rhodecode.lib.exceptions import UserGroupAssignedException,\
41 41 RepoGroupAssignmentError
42 42 from rhodecode.lib.utils import jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
46 46 HasPermissionAnyDecorator, XHRRequired)
47 47 from rhodecode.lib.base import BaseController, render
48 48 from rhodecode.model.permission import PermissionModel
49 49 from rhodecode.model.scm import UserGroupList
50 50 from rhodecode.model.user_group import UserGroupModel
51 51 from rhodecode.model.db import (
52 52 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
53 53 from rhodecode.model.forms import (
54 54 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
55 55 UserPermissionsForm)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class UserGroupsController(BaseController):
63 63 """REST Controller styled on the Atom Publishing Protocol"""
64 64
65 65 @LoginRequired()
66 66 def __before__(self):
67 67 super(UserGroupsController, self).__before__()
68 68 c.available_permissions = config['available_permissions']
69 69 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
70 70
71 71 def __load_data(self, user_group_id):
72 72 c.group_members_obj = [x.user for x in c.user_group.members]
73 73 c.group_members_obj.sort(key=lambda u: u.username.lower())
74 74 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
75 75
76 76 def __load_defaults(self, user_group_id):
77 77 """
78 78 Load defaults settings for edit, and update
79 79
80 80 :param user_group_id:
81 81 """
82 82 user_group = UserGroup.get_or_404(user_group_id)
83 83 data = user_group.get_dict()
84 84 # fill owner
85 85 if user_group.user:
86 86 data.update({'user': user_group.user.username})
87 87 else:
88 88 replacement_user = User.get_first_super_admin().username
89 89 data.update({'user': replacement_user})
90 90 return data
91 91
92 92 def _revoke_perms_on_yourself(self, form_result):
93 93 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
94 94 form_result['perm_updates'])
95 95 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
96 96 form_result['perm_additions'])
97 97 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
98 98 form_result['perm_deletions'])
99 99 admin_perm = 'usergroup.admin'
100 100 if _updates and _updates[0][1] != admin_perm or \
101 101 _additions and _additions[0][1] != admin_perm or \
102 102 _deletions and _deletions[0][1] != admin_perm:
103 103 return True
104 104 return False
105 105
106 106 # permission check inside
107 107 @NotAnonymous()
108 108 def index(self):
109 109
110 110 from rhodecode.lib.utils import PartialRenderer
111 111 _render = PartialRenderer('data_table/_dt_elements.mako')
112 112
113 113 def user_group_name(user_group_id, user_group_name):
114 114 return _render("user_group_name", user_group_id, user_group_name)
115 115
116 116 def user_group_actions(user_group_id, user_group_name):
117 117 return _render("user_group_actions", user_group_id, user_group_name)
118 118
119 119 # json generate
120 120 group_iter = UserGroupList(UserGroup.query().all(),
121 121 perm_set=['usergroup.admin'])
122 122
123 123 user_groups_data = []
124 124 for user_gr in group_iter:
125 125 user_groups_data.append({
126 126 "group_name": user_group_name(
127 127 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
128 128 "group_name_raw": user_gr.users_group_name,
129 129 "desc": h.escape(user_gr.user_group_description),
130 130 "members": len(user_gr.members),
131 131 "sync": user_gr.group_data.get('extern_type'),
132 132 "active": h.bool2icon(user_gr.users_group_active),
133 133 "owner": h.escape(h.link_to_user(user_gr.user.username)),
134 134 "action": user_group_actions(
135 135 user_gr.users_group_id, user_gr.users_group_name)
136 136 })
137 137
138 138 c.data = json.dumps(user_groups_data)
139 139 return render('admin/user_groups/user_groups.mako')
140 140
141 141 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
142 142 @auth.CSRFRequired()
143 143 def create(self):
144 144
145 145 users_group_form = UserGroupForm()()
146 146 try:
147 147 form_result = users_group_form.to_python(dict(request.POST))
148 148 user_group = UserGroupModel().create(
149 149 name=form_result['users_group_name'],
150 150 description=form_result['user_group_description'],
151 151 owner=c.rhodecode_user.user_id,
152 152 active=form_result['users_group_active'])
153 153 Session().flush()
154 154 creation_data = user_group.get_api_data()
155 155 user_group_name = form_result['users_group_name']
156 156
157 157 audit_logger.store_web(
158 158 'user_group.create', action_data={'data': creation_data},
159 159 user=c.rhodecode_user)
160 160
161 161 user_group_link = h.link_to(
162 162 h.escape(user_group_name),
163 163 url('edit_users_group', user_group_id=user_group.users_group_id))
164 164 h.flash(h.literal(_('Created user group %(user_group_link)s')
165 165 % {'user_group_link': user_group_link}),
166 166 category='success')
167 167 Session().commit()
168 168 except formencode.Invalid as errors:
169 169 return htmlfill.render(
170 170 render('admin/user_groups/user_group_add.mako'),
171 171 defaults=errors.value,
172 172 errors=errors.error_dict or {},
173 173 prefix_error=False,
174 174 encoding="UTF-8",
175 175 force_defaults=False)
176 176 except Exception:
177 177 log.exception("Exception creating user group")
178 178 h.flash(_('Error occurred during creation of user group %s') \
179 179 % request.POST.get('users_group_name'), category='error')
180 180
181 181 return redirect(
182 182 url('edit_users_group', user_group_id=user_group.users_group_id))
183 183
184 184 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
185 185 def new(self):
186 186 """GET /user_groups/new: Form to create a new item"""
187 187 # url('new_users_group')
188 188 return render('admin/user_groups/user_group_add.mako')
189 189
190 190 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
191 191 @auth.CSRFRequired()
192 192 def update(self, user_group_id):
193 193
194 194 user_group_id = safe_int(user_group_id)
195 195 c.user_group = UserGroup.get_or_404(user_group_id)
196 196 c.active = 'settings'
197 197 self.__load_data(user_group_id)
198 198
199 199 users_group_form = UserGroupForm(
200 200 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
201 201
202 202 old_values = c.user_group.get_api_data()
203 203 try:
204 204 form_result = users_group_form.to_python(request.POST)
205 205 pstruct = peppercorn.parse(request.POST.items())
206 206 form_result['users_group_members'] = pstruct['user_group_members']
207 207
208 UserGroupModel().update(c.user_group, form_result)
208 user_group, added_members, removed_members = \
209 UserGroupModel().update(c.user_group, form_result)
209 210 updated_user_group = form_result['users_group_name']
210 211
211 212 audit_logger.store_web(
212 213 'user_group.edit', action_data={'old_data': old_values},
213 214 user=c.rhodecode_user)
214 215
216 # TODO(marcink): use added/removed to set user_group.edit.member.add
217
215 218 h.flash(_('Updated user group %s') % updated_user_group,
216 219 category='success')
217 220 Session().commit()
218 221 except formencode.Invalid as errors:
219 222 defaults = errors.value
220 223 e = errors.error_dict or {}
221 224
222 225 return htmlfill.render(
223 226 render('admin/user_groups/user_group_edit.mako'),
224 227 defaults=defaults,
225 228 errors=e,
226 229 prefix_error=False,
227 230 encoding="UTF-8",
228 231 force_defaults=False)
229 232 except Exception:
230 233 log.exception("Exception during update of user group")
231 234 h.flash(_('Error occurred during update of user group %s')
232 235 % request.POST.get('users_group_name'), category='error')
233 236
234 237 return redirect(url('edit_users_group', user_group_id=user_group_id))
235 238
236 239 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
237 240 @auth.CSRFRequired()
238 241 def delete(self, user_group_id):
239 242 user_group_id = safe_int(user_group_id)
240 243 c.user_group = UserGroup.get_or_404(user_group_id)
241 244 force = str2bool(request.POST.get('force'))
242 245
243 246 old_values = c.user_group.get_api_data()
244 247 try:
245 248 UserGroupModel().delete(c.user_group, force=force)
246 249 audit_logger.store_web(
247 250 'user.delete', action_data={'old_data': old_values},
248 251 user=c.rhodecode_user)
249 252 Session().commit()
250 253 h.flash(_('Successfully deleted user group'), category='success')
251 254 except UserGroupAssignedException as e:
252 255 h.flash(str(e), category='error')
253 256 except Exception:
254 257 log.exception("Exception during deletion of user group")
255 258 h.flash(_('An error occurred during deletion of user group'),
256 259 category='error')
257 260 return redirect(url('users_groups'))
258 261
259 262 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
260 263 def edit(self, user_group_id):
261 264 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
262 265 # url('edit_users_group', user_group_id=ID)
263 266
264 267 user_group_id = safe_int(user_group_id)
265 268 c.user_group = UserGroup.get_or_404(user_group_id)
266 269 c.active = 'settings'
267 270 self.__load_data(user_group_id)
268 271
269 272 defaults = self.__load_defaults(user_group_id)
270 273
271 274 return htmlfill.render(
272 275 render('admin/user_groups/user_group_edit.mako'),
273 276 defaults=defaults,
274 277 encoding="UTF-8",
275 278 force_defaults=False
276 279 )
277 280
278 281 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
279 282 def edit_perms(self, user_group_id):
280 283 user_group_id = safe_int(user_group_id)
281 284 c.user_group = UserGroup.get_or_404(user_group_id)
282 285 c.active = 'perms'
283 286
284 287 defaults = {}
285 288 # fill user group users
286 289 for p in c.user_group.user_user_group_to_perm:
287 290 defaults.update({'u_perm_%s' % p.user.user_id:
288 291 p.permission.permission_name})
289 292
290 293 for p in c.user_group.user_group_user_group_to_perm:
291 294 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
292 295 p.permission.permission_name})
293 296
294 297 return htmlfill.render(
295 298 render('admin/user_groups/user_group_edit.mako'),
296 299 defaults=defaults,
297 300 encoding="UTF-8",
298 301 force_defaults=False
299 302 )
300 303
301 304 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
302 305 @auth.CSRFRequired()
303 306 def update_perms(self, user_group_id):
304 307 """
305 308 grant permission for given usergroup
306 309
307 310 :param user_group_id:
308 311 """
309 312 user_group_id = safe_int(user_group_id)
310 313 c.user_group = UserGroup.get_or_404(user_group_id)
311 314 form = UserGroupPermsForm()().to_python(request.POST)
312 315
313 316 if not c.rhodecode_user.is_admin:
314 317 if self._revoke_perms_on_yourself(form):
315 318 msg = _('Cannot change permission for yourself as admin')
316 319 h.flash(msg, category='warning')
317 320 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
318 321
319 322 try:
320 323 UserGroupModel().update_permissions(user_group_id,
321 324 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
322 325 except RepoGroupAssignmentError:
323 326 h.flash(_('Target group cannot be the same'), category='error')
324 327 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
325 328
326 329 # TODO(marcink): implement global permissions
327 330 # audit_log.store_web('user_group.edit.permissions')
328 331 Session().commit()
329 332 h.flash(_('User Group permissions updated'), category='success')
330 333 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
331 334
332 335 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
333 336 def edit_perms_summary(self, user_group_id):
334 337 user_group_id = safe_int(user_group_id)
335 338 c.user_group = UserGroup.get_or_404(user_group_id)
336 339 c.active = 'perms_summary'
337 340 permissions = {
338 341 'repositories': {},
339 342 'repositories_groups': {},
340 343 }
341 344 ugroup_repo_perms = UserGroupRepoToPerm.query()\
342 345 .options(joinedload(UserGroupRepoToPerm.permission))\
343 346 .options(joinedload(UserGroupRepoToPerm.repository))\
344 347 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
345 348 .all()
346 349
347 350 for gr in ugroup_repo_perms:
348 351 permissions['repositories'][gr.repository.repo_name] \
349 352 = gr.permission.permission_name
350 353
351 354 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
352 355 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
353 356 .options(joinedload(UserGroupRepoGroupToPerm.group))\
354 357 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
355 358 .all()
356 359
357 360 for gr in ugroup_group_perms:
358 361 permissions['repositories_groups'][gr.group.group_name] \
359 362 = gr.permission.permission_name
360 363 c.permissions = permissions
361 364 return render('admin/user_groups/user_group_edit.mako')
362 365
363 366 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
364 367 def edit_global_perms(self, user_group_id):
365 368 user_group_id = safe_int(user_group_id)
366 369 c.user_group = UserGroup.get_or_404(user_group_id)
367 370 c.active = 'global_perms'
368 371
369 372 c.default_user = User.get_default_user()
370 373 defaults = c.user_group.get_dict()
371 374 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
372 375 defaults.update(c.user_group.get_default_perms())
373 376
374 377 return htmlfill.render(
375 378 render('admin/user_groups/user_group_edit.mako'),
376 379 defaults=defaults,
377 380 encoding="UTF-8",
378 381 force_defaults=False
379 382 )
380 383
381 384 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
382 385 @auth.CSRFRequired()
383 386 def update_global_perms(self, user_group_id):
384 387 user_group_id = safe_int(user_group_id)
385 388 user_group = UserGroup.get_or_404(user_group_id)
386 389 c.active = 'global_perms'
387 390
388 391 try:
389 392 # first stage that verifies the checkbox
390 393 _form = UserIndividualPermissionsForm()
391 394 form_result = _form.to_python(dict(request.POST))
392 395 inherit_perms = form_result['inherit_default_permissions']
393 396 user_group.inherit_default_permissions = inherit_perms
394 397 Session().add(user_group)
395 398
396 399 if not inherit_perms:
397 400 # only update the individual ones if we un check the flag
398 401 _form = UserPermissionsForm(
399 402 [x[0] for x in c.repo_create_choices],
400 403 [x[0] for x in c.repo_create_on_write_choices],
401 404 [x[0] for x in c.repo_group_create_choices],
402 405 [x[0] for x in c.user_group_create_choices],
403 406 [x[0] for x in c.fork_choices],
404 407 [x[0] for x in c.inherit_default_permission_choices])()
405 408
406 409 form_result = _form.to_python(dict(request.POST))
407 410 form_result.update({'perm_user_group_id': user_group.users_group_id})
408 411
409 412 PermissionModel().update_user_group_permissions(form_result)
410 413
411 414 Session().commit()
412 415 h.flash(_('User Group global permissions updated successfully'),
413 416 category='success')
414 417
415 418 except formencode.Invalid as errors:
416 419 defaults = errors.value
417 420 c.user_group = user_group
418 421 return htmlfill.render(
419 422 render('admin/user_groups/user_group_edit.mako'),
420 423 defaults=defaults,
421 424 errors=errors.error_dict or {},
422 425 prefix_error=False,
423 426 encoding="UTF-8",
424 427 force_defaults=False)
425 428 except Exception:
426 429 log.exception("Exception during permissions saving")
427 430 h.flash(_('An error occurred during permissions saving'),
428 431 category='error')
429 432
430 433 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
431 434
432 435 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
433 436 def edit_advanced(self, user_group_id):
434 437 user_group_id = safe_int(user_group_id)
435 438 c.user_group = UserGroup.get_or_404(user_group_id)
436 439 c.active = 'advanced'
437 440 c.group_members_obj = sorted(
438 441 (x.user for x in c.user_group.members),
439 442 key=lambda u: u.username.lower())
440 443
441 444 c.group_to_repos = sorted(
442 445 (x.repository for x in c.user_group.users_group_repo_to_perm),
443 446 key=lambda u: u.repo_name.lower())
444 447
445 448 c.group_to_repo_groups = sorted(
446 449 (x.group for x in c.user_group.users_group_repo_group_to_perm),
447 450 key=lambda u: u.group_name.lower())
448 451
449 452 return render('admin/user_groups/user_group_edit.mako')
450 453
451 454 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
452 455 def edit_advanced_set_synchronization(self, user_group_id):
453 456 user_group_id = safe_int(user_group_id)
454 457 user_group = UserGroup.get_or_404(user_group_id)
455 458
456 459 existing = user_group.group_data.get('extern_type')
457 460
458 461 if existing:
459 462 new_state = user_group.group_data
460 463 new_state['extern_type'] = None
461 464 else:
462 465 new_state = user_group.group_data
463 466 new_state['extern_type'] = 'manual'
464 467 new_state['extern_type_set_by'] = c.rhodecode_user.username
465 468
466 469 try:
467 470 user_group.group_data = new_state
468 471 Session().add(user_group)
469 472 Session().commit()
470 473
471 474 h.flash(_('User Group synchronization updated successfully'),
472 475 category='success')
473 476 except Exception:
474 477 log.exception("Exception during sync settings saving")
475 478 h.flash(_('An error occurred during synchronization update'),
476 479 category='error')
477 480
478 481 return redirect(
479 482 url('edit_user_group_advanced', user_group_id=user_group_id))
480 483
481 484 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
482 485 @XHRRequired()
483 486 @jsonify
484 487 def user_group_members(self, user_group_id):
485 488 """
486 489 Return members of given user group
487 490 """
488 491 user_group_id = safe_int(user_group_id)
489 492 user_group = UserGroup.get_or_404(user_group_id)
490 493 group_members_obj = sorted((x.user for x in user_group.members),
491 494 key=lambda u: u.username.lower())
492 495
493 496 group_members = [
494 497 {
495 498 'id': user.user_id,
496 499 'first_name': user.first_name,
497 500 'last_name': user.last_name,
498 501 'username': user.username,
499 502 'icon_link': h.gravatar_url(user.email, 30),
500 503 'value_display': h.person(user.email),
501 504 'value': user.username,
502 505 'value_type': 'user',
503 506 'active': user.active,
504 507 }
505 508 for user in group_members_obj
506 509 ]
507 510
508 511 return {
509 512 'members': group_members
510 513 }
@@ -1,1110 +1,1110 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Files controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import itertools
26 26 import logging
27 27 import os
28 28 import shutil
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 35
36 36 from rhodecode.controllers.utils import parse_path_ref
37 37 from rhodecode.lib import diffs, helpers as h, caches
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils import jsonify
42 42 from rhodecode.lib.utils2 import (
43 43 convert_line_endings, detect_mode, safe_str, str2bool)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
46 46 from rhodecode.lib.base import BaseRepoController, render
47 47 from rhodecode.lib.vcs import path as vcspath
48 48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 49 from rhodecode.lib.vcs.conf import settings
50 50 from rhodecode.lib.vcs.exceptions import (
51 51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 53 NodeDoesNotExistError, CommitError, NodeError)
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55
56 56 from rhodecode.model.repo import RepoModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.db import Repository
59 59
60 60 from rhodecode.controllers.changeset import (
61 61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
62 62 from rhodecode.lib.exceptions import NonRelativePathError
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class FilesController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(FilesController, self).__before__()
71 71 c.cut_off_limit = self.cut_off_limit_file
72 72
73 73 def _get_default_encoding(self):
74 74 enc_list = getattr(c, 'default_encodings', [])
75 75 return enc_list[0] if enc_list else 'UTF-8'
76 76
77 77 def __get_commit_or_redirect(self, commit_id, repo_name,
78 78 redirect_after=True):
79 79 """
80 80 This is a safe way to get commit. If an error occurs it redirects to
81 81 tip with proper message
82 82
83 83 :param commit_id: id of commit to fetch
84 84 :param repo_name: repo name to redirect after
85 85 :param redirect_after: toggle redirection
86 86 """
87 87 try:
88 88 return c.rhodecode_repo.get_commit(commit_id)
89 89 except EmptyRepositoryError:
90 90 if not redirect_after:
91 91 return None
92 92 url_ = url('files_add_home',
93 93 repo_name=c.repo_name,
94 94 revision=0, f_path='', anchor='edit')
95 95 if h.HasRepoPermissionAny(
96 96 'repository.write', 'repository.admin')(c.repo_name):
97 97 add_new = h.link_to(
98 98 _('Click here to add a new file.'),
99 99 url_, class_="alert-link")
100 100 else:
101 101 add_new = ""
102 102 h.flash(h.literal(
103 103 _('There are no files yet. %s') % add_new), category='warning')
104 104 redirect(h.route_path('repo_summary', repo_name=repo_name))
105 105 except (CommitDoesNotExistError, LookupError):
106 106 msg = _('No such commit exists for this repository')
107 107 h.flash(msg, category='error')
108 108 raise HTTPNotFound()
109 109 except RepositoryError as e:
110 110 h.flash(safe_str(e), category='error')
111 111 raise HTTPNotFound()
112 112
113 113 def __get_filenode_or_redirect(self, repo_name, commit, path):
114 114 """
115 115 Returns file_node, if error occurs or given path is directory,
116 116 it'll redirect to top level path
117 117
118 118 :param repo_name: repo_name
119 119 :param commit: given commit
120 120 :param path: path to lookup
121 121 """
122 122 try:
123 123 file_node = commit.get_node(path)
124 124 if file_node.is_dir():
125 125 raise RepositoryError('The given path is a directory')
126 126 except CommitDoesNotExistError:
127 127 log.exception('No such commit exists for this repository')
128 128 h.flash(_('No such commit exists for this repository'), category='error')
129 129 raise HTTPNotFound()
130 130 except RepositoryError as e:
131 131 h.flash(safe_str(e), category='error')
132 132 raise HTTPNotFound()
133 133
134 134 return file_node
135 135
136 136 def __get_tree_cache_manager(self, repo_name, namespace_type):
137 137 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
138 138 return caches.get_cache_manager('repo_cache_long', _namespace)
139 139
140 140 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
141 141 full_load=False, force=False):
142 142 def _cached_tree():
143 143 log.debug('Generating cached file tree for %s, %s, %s',
144 144 repo_name, commit_id, f_path)
145 145 c.full_load = full_load
146 146 return render('files/files_browser_tree.mako')
147 147
148 148 cache_manager = self.__get_tree_cache_manager(
149 149 repo_name, caches.FILE_TREE)
150 150
151 151 cache_key = caches.compute_key_from_params(
152 152 repo_name, commit_id, f_path)
153 153
154 154 if force:
155 155 # we want to force recompute of caches
156 156 cache_manager.remove_value(cache_key)
157 157
158 158 return cache_manager.get(cache_key, createfunc=_cached_tree)
159 159
160 160 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
161 161 def _cached_nodes():
162 162 log.debug('Generating cached nodelist for %s, %s, %s',
163 163 repo_name, commit_id, f_path)
164 164 _d, _f = ScmModel().get_nodes(
165 165 repo_name, commit_id, f_path, flat=False)
166 166 return _d + _f
167 167
168 168 cache_manager = self.__get_tree_cache_manager(
169 169 repo_name, caches.FILE_SEARCH_TREE_META)
170 170
171 171 cache_key = caches.compute_key_from_params(
172 172 repo_name, commit_id, f_path)
173 173 return cache_manager.get(cache_key, createfunc=_cached_nodes)
174 174
175 175 @LoginRequired()
176 176 @HasRepoPermissionAnyDecorator(
177 177 'repository.read', 'repository.write', 'repository.admin')
178 178 def index(
179 179 self, repo_name, revision, f_path, annotate=False, rendered=False):
180 180 commit_id = revision
181 181
182 182 # redirect to given commit_id from form if given
183 183 get_commit_id = request.GET.get('at_rev', None)
184 184 if get_commit_id:
185 185 self.__get_commit_or_redirect(get_commit_id, repo_name)
186 186
187 187 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
188 188 c.branch = request.GET.get('branch', None)
189 189 c.f_path = f_path
190 190 c.annotate = annotate
191 191 # default is false, but .rst/.md files later are autorendered, we can
192 192 # overwrite autorendering by setting this GET flag
193 193 c.renderer = rendered or not request.GET.get('no-render', False)
194 194
195 195 # prev link
196 196 try:
197 197 prev_commit = c.commit.prev(c.branch)
198 198 c.prev_commit = prev_commit
199 199 c.url_prev = url('files_home', repo_name=c.repo_name,
200 200 revision=prev_commit.raw_id, f_path=f_path)
201 201 if c.branch:
202 202 c.url_prev += '?branch=%s' % c.branch
203 203 except (CommitDoesNotExistError, VCSError):
204 204 c.url_prev = '#'
205 205 c.prev_commit = EmptyCommit()
206 206
207 207 # next link
208 208 try:
209 209 next_commit = c.commit.next(c.branch)
210 210 c.next_commit = next_commit
211 211 c.url_next = url('files_home', repo_name=c.repo_name,
212 212 revision=next_commit.raw_id, f_path=f_path)
213 213 if c.branch:
214 214 c.url_next += '?branch=%s' % c.branch
215 215 except (CommitDoesNotExistError, VCSError):
216 216 c.url_next = '#'
217 217 c.next_commit = EmptyCommit()
218 218
219 219 # files or dirs
220 220 try:
221 221 c.file = c.commit.get_node(f_path)
222 222 c.file_author = True
223 223 c.file_tree = ''
224 224 if c.file.is_file():
225 225 c.lf_node = c.file.get_largefile_node()
226 226
227 227 c.file_source_page = 'true'
228 228 c.file_last_commit = c.file.last_commit
229 229 if c.file.size < self.cut_off_limit_file:
230 230 if c.annotate: # annotation has precedence over renderer
231 231 c.annotated_lines = filenode_as_annotated_lines_tokens(
232 232 c.file
233 233 )
234 234 else:
235 235 c.renderer = (
236 236 c.renderer and h.renderer_from_filename(c.file.path)
237 237 )
238 238 if not c.renderer:
239 239 c.lines = filenode_as_lines_tokens(c.file)
240 240
241 241 c.on_branch_head = self._is_valid_head(
242 242 commit_id, c.rhodecode_repo)
243 243
244 244 branch = c.commit.branch if (
245 245 c.commit.branch and '/' not in c.commit.branch) else None
246 246 c.branch_or_raw_id = branch or c.commit.raw_id
247 247 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
248 248
249 249 author = c.file_last_commit.author
250 250 c.authors = [(h.email(author),
251 251 h.person(author, 'username_or_name_or_email'))]
252 252 else:
253 253 c.file_source_page = 'false'
254 254 c.authors = []
255 255 c.file_tree = self._get_tree_at_commit(
256 256 repo_name, c.commit.raw_id, f_path)
257 257
258 258 except RepositoryError as e:
259 259 h.flash(safe_str(e), category='error')
260 260 raise HTTPNotFound()
261 261
262 262 if request.environ.get('HTTP_X_PJAX'):
263 263 return render('files/files_pjax.mako')
264 264
265 265 return render('files/files.mako')
266 266
267 267 @LoginRequired()
268 268 @HasRepoPermissionAnyDecorator(
269 269 'repository.read', 'repository.write', 'repository.admin')
270 270 def annotate_previous(self, repo_name, revision, f_path):
271 271
272 272 commit_id = revision
273 273 commit = self.__get_commit_or_redirect(commit_id, repo_name)
274 274 prev_commit_id = commit.raw_id
275 275
276 276 f_path = f_path
277 277 is_file = False
278 278 try:
279 279 _file = commit.get_node(f_path)
280 280 is_file = _file.is_file()
281 281 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
282 282 pass
283 283
284 284 if is_file:
285 285 history = commit.get_file_history(f_path)
286 286 prev_commit_id = history[1].raw_id \
287 287 if len(history) > 1 else prev_commit_id
288 288
289 289 return redirect(h.url(
290 290 'files_annotate_home', repo_name=repo_name,
291 291 revision=prev_commit_id, f_path=f_path))
292 292
293 293 @LoginRequired()
294 294 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
295 295 'repository.admin')
296 296 @jsonify
297 297 def history(self, repo_name, revision, f_path):
298 298 commit = self.__get_commit_or_redirect(revision, repo_name)
299 299 f_path = f_path
300 300 _file = commit.get_node(f_path)
301 301 if _file.is_file():
302 302 file_history, _hist = self._get_node_history(commit, f_path)
303 303
304 304 res = []
305 305 for obj in file_history:
306 306 res.append({
307 307 'text': obj[1],
308 308 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
309 309 })
310 310
311 311 data = {
312 312 'more': False,
313 313 'results': res
314 314 }
315 315 return data
316 316
317 317 @LoginRequired()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 def authors(self, repo_name, revision, f_path):
321 321 commit = self.__get_commit_or_redirect(revision, repo_name)
322 322 file_node = commit.get_node(f_path)
323 323 if file_node.is_file():
324 324 c.file_last_commit = file_node.last_commit
325 325 if request.GET.get('annotate') == '1':
326 326 # use _hist from annotation if annotation mode is on
327 327 commit_ids = set(x[1] for x in file_node.annotate)
328 328 _hist = (
329 329 c.rhodecode_repo.get_commit(commit_id)
330 330 for commit_id in commit_ids)
331 331 else:
332 332 _f_history, _hist = self._get_node_history(commit, f_path)
333 333 c.file_author = False
334 334 c.authors = []
335 335 for author in set(commit.author for commit in _hist):
336 336 c.authors.append((
337 337 h.email(author),
338 338 h.person(author, 'username_or_name_or_email')))
339 339 return render('files/file_authors_box.mako')
340 340
341 341 @LoginRequired()
342 342 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 343 'repository.admin')
344 344 def rawfile(self, repo_name, revision, f_path):
345 345 """
346 346 Action for download as raw
347 347 """
348 348 commit = self.__get_commit_or_redirect(revision, repo_name)
349 349 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
350 350
351 351 if request.GET.get('lf'):
352 352 # only if lf get flag is passed, we download this file
353 353 # as LFS/Largefile
354 354 lf_node = file_node.get_largefile_node()
355 355 if lf_node:
356 356 # overwrite our pointer with the REAL large-file
357 357 file_node = lf_node
358 358
359 359 response.content_disposition = 'attachment; filename=%s' % \
360 360 safe_str(f_path.split(Repository.NAME_SEP)[-1])
361 361
362 362 response.content_type = file_node.mimetype
363 363 charset = self._get_default_encoding()
364 364 if charset:
365 365 response.charset = charset
366 366
367 367 return file_node.content
368 368
369 369 @LoginRequired()
370 370 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
371 371 'repository.admin')
372 372 def raw(self, repo_name, revision, f_path):
373 373 """
374 374 Action for show as raw, some mimetypes are "rendered",
375 375 those include images, icons.
376 376 """
377 377 commit = self.__get_commit_or_redirect(revision, repo_name)
378 378 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
379 379
380 380 raw_mimetype_mapping = {
381 381 # map original mimetype to a mimetype used for "show as raw"
382 382 # you can also provide a content-disposition to override the
383 383 # default "attachment" disposition.
384 384 # orig_type: (new_type, new_dispo)
385 385
386 386 # show images inline:
387 387 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
388 388 # for example render an SVG with javascript inside or even render
389 389 # HTML.
390 390 'image/x-icon': ('image/x-icon', 'inline'),
391 391 'image/png': ('image/png', 'inline'),
392 392 'image/gif': ('image/gif', 'inline'),
393 393 'image/jpeg': ('image/jpeg', 'inline'),
394 394 'application/pdf': ('application/pdf', 'inline'),
395 395 }
396 396
397 397 mimetype = file_node.mimetype
398 398 try:
399 399 mimetype, dispo = raw_mimetype_mapping[mimetype]
400 400 except KeyError:
401 401 # we don't know anything special about this, handle it safely
402 402 if file_node.is_binary:
403 403 # do same as download raw for binary files
404 404 mimetype, dispo = 'application/octet-stream', 'attachment'
405 405 else:
406 406 # do not just use the original mimetype, but force text/plain,
407 407 # otherwise it would serve text/html and that might be unsafe.
408 408 # Note: underlying vcs library fakes text/plain mimetype if the
409 409 # mimetype can not be determined and it thinks it is not
410 410 # binary.This might lead to erroneous text display in some
411 411 # cases, but helps in other cases, like with text files
412 412 # without extension.
413 413 mimetype, dispo = 'text/plain', 'inline'
414 414
415 415 if dispo == 'attachment':
416 416 dispo = 'attachment; filename=%s' % safe_str(
417 417 f_path.split(os.sep)[-1])
418 418
419 419 response.content_disposition = dispo
420 420 response.content_type = mimetype
421 421 charset = self._get_default_encoding()
422 422 if charset:
423 423 response.charset = charset
424 424 return file_node.content
425 425
426 426 @CSRFRequired()
427 427 @LoginRequired()
428 428 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
429 429 def delete(self, repo_name, revision, f_path):
430 430 commit_id = revision
431 431
432 432 repo = c.rhodecode_db_repo
433 433 if repo.enable_locking and repo.locked[0]:
434 434 h.flash(_('This repository has been locked by %s on %s')
435 435 % (h.person_by_id(repo.locked[0]),
436 436 h.format_date(h.time_to_datetime(repo.locked[1]))),
437 437 'warning')
438 438 return redirect(h.url('files_home',
439 439 repo_name=repo_name, revision='tip'))
440 440
441 441 if not self._is_valid_head(commit_id, repo.scm_instance()):
442 442 h.flash(_('You can only delete files with revision '
443 443 'being a valid branch '), category='warning')
444 444 return redirect(h.url('files_home',
445 445 repo_name=repo_name, revision='tip',
446 446 f_path=f_path))
447 447
448 448 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
449 449 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
450 450
451 451 c.default_message = _(
452 452 'Deleted file {} via RhodeCode Enterprise').format(f_path)
453 453 c.f_path = f_path
454 454 node_path = f_path
455 455 author = c.rhodecode_user.full_contact
456 456 message = request.POST.get('message') or c.default_message
457 457 try:
458 458 nodes = {
459 459 node_path: {
460 460 'content': ''
461 461 }
462 462 }
463 463 self.scm_model.delete_nodes(
464 464 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
465 465 message=message,
466 466 nodes=nodes,
467 467 parent_commit=c.commit,
468 468 author=author,
469 469 )
470 470
471 471 h.flash(
472 472 _('Successfully deleted file `{}`').format(
473 473 h.escape(f_path)), category='success')
474 474 except Exception:
475 475 msg = _('Error occurred during commit')
476 476 log.exception(msg)
477 477 h.flash(msg, category='error')
478 478 return redirect(url('changeset_home',
479 479 repo_name=c.repo_name, revision='tip'))
480 480
481 481 @LoginRequired()
482 482 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
483 483 def delete_home(self, repo_name, revision, f_path):
484 484 commit_id = revision
485 485
486 486 repo = c.rhodecode_db_repo
487 487 if repo.enable_locking and repo.locked[0]:
488 488 h.flash(_('This repository has been locked by %s on %s')
489 489 % (h.person_by_id(repo.locked[0]),
490 490 h.format_date(h.time_to_datetime(repo.locked[1]))),
491 491 'warning')
492 492 return redirect(h.url('files_home',
493 493 repo_name=repo_name, revision='tip'))
494 494
495 495 if not self._is_valid_head(commit_id, repo.scm_instance()):
496 496 h.flash(_('You can only delete files with revision '
497 497 'being a valid branch '), category='warning')
498 498 return redirect(h.url('files_home',
499 499 repo_name=repo_name, revision='tip',
500 500 f_path=f_path))
501 501
502 502 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
503 503 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
504 504
505 505 c.default_message = _(
506 506 'Deleted file {} via RhodeCode Enterprise').format(f_path)
507 507 c.f_path = f_path
508 508
509 509 return render('files/files_delete.mako')
510 510
511 511 @CSRFRequired()
512 512 @LoginRequired()
513 513 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
514 514 def edit(self, repo_name, revision, f_path):
515 515 commit_id = revision
516 516
517 517 repo = c.rhodecode_db_repo
518 518 if repo.enable_locking and repo.locked[0]:
519 519 h.flash(_('This repository has been locked by %s on %s')
520 520 % (h.person_by_id(repo.locked[0]),
521 521 h.format_date(h.time_to_datetime(repo.locked[1]))),
522 522 'warning')
523 523 return redirect(h.url('files_home',
524 524 repo_name=repo_name, revision='tip'))
525 525
526 526 if not self._is_valid_head(commit_id, repo.scm_instance()):
527 527 h.flash(_('You can only edit files with revision '
528 528 'being a valid branch '), category='warning')
529 529 return redirect(h.url('files_home',
530 530 repo_name=repo_name, revision='tip',
531 531 f_path=f_path))
532 532
533 533 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
534 534 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
535 535
536 536 if c.file.is_binary:
537 537 return redirect(url('files_home', repo_name=c.repo_name,
538 538 revision=c.commit.raw_id, f_path=f_path))
539 539 c.default_message = _(
540 540 'Edited file {} via RhodeCode Enterprise').format(f_path)
541 541 c.f_path = f_path
542 542 old_content = c.file.content
543 543 sl = old_content.splitlines(1)
544 544 first_line = sl[0] if sl else ''
545 545
546 546 # modes: 0 - Unix, 1 - Mac, 2 - DOS
547 547 mode = detect_mode(first_line, 0)
548 548 content = convert_line_endings(request.POST.get('content', ''), mode)
549 549
550 550 message = request.POST.get('message') or c.default_message
551 551 org_f_path = c.file.unicode_path
552 552 filename = request.POST['filename']
553 553 org_filename = c.file.name
554 554
555 555 if content == old_content and filename == org_filename:
556 556 h.flash(_('No changes'), category='warning')
557 557 return redirect(url('changeset_home', repo_name=c.repo_name,
558 558 revision='tip'))
559 559 try:
560 560 mapping = {
561 561 org_f_path: {
562 562 'org_filename': org_f_path,
563 563 'filename': os.path.join(c.file.dir_path, filename),
564 564 'content': content,
565 565 'lexer': '',
566 566 'op': 'mod',
567 567 }
568 568 }
569 569
570 570 ScmModel().update_nodes(
571 571 user=c.rhodecode_user.user_id,
572 572 repo=c.rhodecode_db_repo,
573 573 message=message,
574 574 nodes=mapping,
575 575 parent_commit=c.commit,
576 576 )
577 577
578 578 h.flash(
579 579 _('Successfully committed changes to file `{}`').format(
580 580 h.escape(f_path)), category='success')
581 581 except Exception:
582 582 log.exception('Error occurred during commit')
583 583 h.flash(_('Error occurred during commit'), category='error')
584 584 return redirect(url('changeset_home',
585 585 repo_name=c.repo_name, revision='tip'))
586 586
587 587 @LoginRequired()
588 588 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
589 589 def edit_home(self, repo_name, revision, f_path):
590 590 commit_id = revision
591 591
592 592 repo = c.rhodecode_db_repo
593 593 if repo.enable_locking and repo.locked[0]:
594 594 h.flash(_('This repository has been locked by %s on %s')
595 595 % (h.person_by_id(repo.locked[0]),
596 596 h.format_date(h.time_to_datetime(repo.locked[1]))),
597 597 'warning')
598 598 return redirect(h.url('files_home',
599 599 repo_name=repo_name, revision='tip'))
600 600
601 601 if not self._is_valid_head(commit_id, repo.scm_instance()):
602 602 h.flash(_('You can only edit files with revision '
603 603 'being a valid branch '), category='warning')
604 604 return redirect(h.url('files_home',
605 605 repo_name=repo_name, revision='tip',
606 606 f_path=f_path))
607 607
608 608 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
609 609 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
610 610
611 611 if c.file.is_binary:
612 612 return redirect(url('files_home', repo_name=c.repo_name,
613 613 revision=c.commit.raw_id, f_path=f_path))
614 614 c.default_message = _(
615 615 'Edited file {} via RhodeCode Enterprise').format(f_path)
616 616 c.f_path = f_path
617 617
618 618 return render('files/files_edit.mako')
619 619
620 620 def _is_valid_head(self, commit_id, repo):
621 621 # check if commit is a branch identifier- basically we cannot
622 622 # create multiple heads via file editing
623 623 valid_heads = repo.branches.keys() + repo.branches.values()
624 624
625 625 if h.is_svn(repo) and not repo.is_empty():
626 626 # Note: Subversion only has one head, we add it here in case there
627 627 # is no branch matched.
628 628 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
629 629
630 630 # check if commit is a branch name or branch hash
631 631 return commit_id in valid_heads
632 632
633 633 @CSRFRequired()
634 634 @LoginRequired()
635 635 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
636 636 def add(self, repo_name, revision, f_path):
637 637 repo = Repository.get_by_repo_name(repo_name)
638 638 if repo.enable_locking and repo.locked[0]:
639 639 h.flash(_('This repository has been locked by %s on %s')
640 640 % (h.person_by_id(repo.locked[0]),
641 641 h.format_date(h.time_to_datetime(repo.locked[1]))),
642 642 'warning')
643 643 return redirect(h.url('files_home',
644 644 repo_name=repo_name, revision='tip'))
645 645
646 646 r_post = request.POST
647 647
648 648 c.commit = self.__get_commit_or_redirect(
649 649 revision, repo_name, redirect_after=False)
650 650 if c.commit is None:
651 651 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
652 652 c.default_message = (_('Added file via RhodeCode Enterprise'))
653 653 c.f_path = f_path
654 654 unix_mode = 0
655 655 content = convert_line_endings(r_post.get('content', ''), unix_mode)
656 656
657 657 message = r_post.get('message') or c.default_message
658 658 filename = r_post.get('filename')
659 659 location = r_post.get('location', '') # dir location
660 660 file_obj = r_post.get('upload_file', None)
661 661
662 662 if file_obj is not None and hasattr(file_obj, 'filename'):
663 663 filename = r_post.get('filename_upload')
664 664 content = file_obj.file
665 665
666 666 if hasattr(content, 'file'):
667 667 # non posix systems store real file under file attr
668 668 content = content.file
669 669
670 670 # If there's no commit, redirect to repo summary
671 671 if type(c.commit) is EmptyCommit:
672 672 redirect_url = h.route_path('repo_summary', repo_name=c.repo_name)
673 673 else:
674 674 redirect_url = url("changeset_home", repo_name=c.repo_name,
675 675 revision='tip')
676 676
677 677 if not filename:
678 678 h.flash(_('No filename'), category='warning')
679 679 return redirect(redirect_url)
680 680
681 681 # extract the location from filename,
682 682 # allows using foo/bar.txt syntax to create subdirectories
683 683 subdir_loc = filename.rsplit('/', 1)
684 684 if len(subdir_loc) == 2:
685 685 location = os.path.join(location, subdir_loc[0])
686 686
687 687 # strip all crap out of file, just leave the basename
688 688 filename = os.path.basename(filename)
689 689 node_path = os.path.join(location, filename)
690 690 author = c.rhodecode_user.full_contact
691 691
692 692 try:
693 693 nodes = {
694 694 node_path: {
695 695 'content': content
696 696 }
697 697 }
698 698 self.scm_model.create_nodes(
699 699 user=c.rhodecode_user.user_id,
700 700 repo=c.rhodecode_db_repo,
701 701 message=message,
702 702 nodes=nodes,
703 703 parent_commit=c.commit,
704 704 author=author,
705 705 )
706 706
707 707 h.flash(
708 708 _('Successfully committed new file `{}`').format(
709 709 h.escape(node_path)), category='success')
710 710 except NonRelativePathError as e:
711 711 h.flash(_(
712 712 'The location specified must be a relative path and must not '
713 713 'contain .. in the path'), category='warning')
714 714 return redirect(url('changeset_home', repo_name=c.repo_name,
715 715 revision='tip'))
716 716 except (NodeError, NodeAlreadyExistsError) as e:
717 717 h.flash(_(h.escape(e)), category='error')
718 718 except Exception:
719 719 log.exception('Error occurred during commit')
720 720 h.flash(_('Error occurred during commit'), category='error')
721 721 return redirect(url('changeset_home',
722 722 repo_name=c.repo_name, revision='tip'))
723 723
724 724 @LoginRequired()
725 725 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
726 726 def add_home(self, repo_name, revision, f_path):
727 727
728 728 repo = Repository.get_by_repo_name(repo_name)
729 729 if repo.enable_locking and repo.locked[0]:
730 730 h.flash(_('This repository has been locked by %s on %s')
731 731 % (h.person_by_id(repo.locked[0]),
732 732 h.format_date(h.time_to_datetime(repo.locked[1]))),
733 733 'warning')
734 734 return redirect(h.url('files_home',
735 735 repo_name=repo_name, revision='tip'))
736 736
737 737 c.commit = self.__get_commit_or_redirect(
738 738 revision, repo_name, redirect_after=False)
739 739 if c.commit is None:
740 740 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
741 741 c.default_message = (_('Added file via RhodeCode Enterprise'))
742 742 c.f_path = f_path
743 743
744 744 return render('files/files_add.mako')
745 745
746 746 @LoginRequired()
747 747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 748 'repository.admin')
749 749 def archivefile(self, repo_name, fname):
750 750 fileformat = None
751 751 commit_id = None
752 752 ext = None
753 753 subrepos = request.GET.get('subrepos') == 'true'
754 754
755 755 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
756 756 archive_spec = fname.split(ext_data[1])
757 757 if len(archive_spec) == 2 and archive_spec[1] == '':
758 758 fileformat = a_type or ext_data[1]
759 759 commit_id = archive_spec[0]
760 760 ext = ext_data[1]
761 761
762 762 dbrepo = RepoModel().get_by_repo_name(repo_name)
763 763 if not dbrepo.enable_downloads:
764 764 return _('Downloads disabled')
765 765
766 766 try:
767 767 commit = c.rhodecode_repo.get_commit(commit_id)
768 768 content_type = settings.ARCHIVE_SPECS[fileformat][0]
769 769 except CommitDoesNotExistError:
770 770 return _('Unknown revision %s') % commit_id
771 771 except EmptyRepositoryError:
772 772 return _('Empty repository')
773 773 except KeyError:
774 774 return _('Unknown archive type')
775 775
776 776 # archive cache
777 777 from rhodecode import CONFIG
778 778
779 779 archive_name = '%s-%s%s%s' % (
780 780 safe_str(repo_name.replace('/', '_')),
781 781 '-sub' if subrepos else '',
782 782 safe_str(commit.short_id), ext)
783 783
784 784 use_cached_archive = False
785 785 archive_cache_enabled = CONFIG.get(
786 786 'archive_cache_dir') and not request.GET.get('no_cache')
787 787
788 788 if archive_cache_enabled:
789 789 # check if we it's ok to write
790 790 if not os.path.isdir(CONFIG['archive_cache_dir']):
791 791 os.makedirs(CONFIG['archive_cache_dir'])
792 792 cached_archive_path = os.path.join(
793 793 CONFIG['archive_cache_dir'], archive_name)
794 794 if os.path.isfile(cached_archive_path):
795 795 log.debug('Found cached archive in %s', cached_archive_path)
796 796 fd, archive = None, cached_archive_path
797 797 use_cached_archive = True
798 798 else:
799 799 log.debug('Archive %s is not yet cached', archive_name)
800 800
801 801 if not use_cached_archive:
802 802 # generate new archive
803 803 fd, archive = tempfile.mkstemp()
804 804 log.debug('Creating new temp archive in %s', archive)
805 805 try:
806 806 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
807 807 except ImproperArchiveTypeError:
808 808 return _('Unknown archive type')
809 809 if archive_cache_enabled:
810 810 # if we generated the archive and we have cache enabled
811 811 # let's use this for future
812 812 log.debug('Storing new archive in %s', cached_archive_path)
813 813 shutil.move(archive, cached_archive_path)
814 814 archive = cached_archive_path
815 815
816 816 # store download action
817 817 audit_logger.store_web(
818 action='repo.archive.download',
819 action_data={'user_agent': request.user_agent,
820 'archive_name': archive_name,
821 'archive_spec': fname,
822 'archive_cached': use_cached_archive},
818 'repo.archive.download', action_data={
819 'user_agent': request.user_agent,
820 'archive_name': archive_name,
821 'archive_spec': fname,
822 'archive_cached': use_cached_archive},
823 823 user=c.rhodecode_user,
824 824 repo=dbrepo,
825 825 commit=True
826 826 )
827 827
828 828 response.content_disposition = str(
829 829 'attachment; filename=%s' % archive_name)
830 830 response.content_type = str(content_type)
831 831
832 832 def get_chunked_archive(archive):
833 833 with open(archive, 'rb') as stream:
834 834 while True:
835 835 data = stream.read(16 * 1024)
836 836 if not data:
837 837 if fd: # fd means we used temporary file
838 838 os.close(fd)
839 839 if not archive_cache_enabled:
840 840 log.debug('Destroying temp archive %s', archive)
841 841 os.remove(archive)
842 842 break
843 843 yield data
844 844
845 845 return get_chunked_archive(archive)
846 846
847 847 @LoginRequired()
848 848 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
849 849 'repository.admin')
850 850 def diff(self, repo_name, f_path):
851 851
852 852 c.action = request.GET.get('diff')
853 853 diff1 = request.GET.get('diff1', '')
854 854 diff2 = request.GET.get('diff2', '')
855 855
856 856 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
857 857
858 858 ignore_whitespace = str2bool(request.GET.get('ignorews'))
859 859 line_context = request.GET.get('context', 3)
860 860
861 861 if not any((diff1, diff2)):
862 862 h.flash(
863 863 'Need query parameter "diff1" or "diff2" to generate a diff.',
864 864 category='error')
865 865 raise HTTPBadRequest()
866 866
867 867 if c.action not in ['download', 'raw']:
868 868 # redirect to new view if we render diff
869 869 return redirect(
870 870 url('compare_url', repo_name=repo_name,
871 871 source_ref_type='rev',
872 872 source_ref=diff1,
873 873 target_repo=c.repo_name,
874 874 target_ref_type='rev',
875 875 target_ref=diff2,
876 876 f_path=f_path))
877 877
878 878 try:
879 879 node1 = self._get_file_node(diff1, path1)
880 880 node2 = self._get_file_node(diff2, f_path)
881 881 except (RepositoryError, NodeError):
882 882 log.exception("Exception while trying to get node from repository")
883 883 return redirect(url(
884 884 'files_home', repo_name=c.repo_name, f_path=f_path))
885 885
886 886 if all(isinstance(node.commit, EmptyCommit)
887 887 for node in (node1, node2)):
888 888 raise HTTPNotFound
889 889
890 890 c.commit_1 = node1.commit
891 891 c.commit_2 = node2.commit
892 892
893 893 if c.action == 'download':
894 894 _diff = diffs.get_gitdiff(node1, node2,
895 895 ignore_whitespace=ignore_whitespace,
896 896 context=line_context)
897 897 diff = diffs.DiffProcessor(_diff, format='gitdiff')
898 898
899 899 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
900 900 response.content_type = 'text/plain'
901 901 response.content_disposition = (
902 902 'attachment; filename=%s' % (diff_name,)
903 903 )
904 904 charset = self._get_default_encoding()
905 905 if charset:
906 906 response.charset = charset
907 907 return diff.as_raw()
908 908
909 909 elif c.action == 'raw':
910 910 _diff = diffs.get_gitdiff(node1, node2,
911 911 ignore_whitespace=ignore_whitespace,
912 912 context=line_context)
913 913 diff = diffs.DiffProcessor(_diff, format='gitdiff')
914 914 response.content_type = 'text/plain'
915 915 charset = self._get_default_encoding()
916 916 if charset:
917 917 response.charset = charset
918 918 return diff.as_raw()
919 919
920 920 else:
921 921 return redirect(
922 922 url('compare_url', repo_name=repo_name,
923 923 source_ref_type='rev',
924 924 source_ref=diff1,
925 925 target_repo=c.repo_name,
926 926 target_ref_type='rev',
927 927 target_ref=diff2,
928 928 f_path=f_path))
929 929
930 930 @LoginRequired()
931 931 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
932 932 'repository.admin')
933 933 def diff_2way(self, repo_name, f_path):
934 934 """
935 935 Kept only to make OLD links work
936 936 """
937 937 diff1 = request.GET.get('diff1', '')
938 938 diff2 = request.GET.get('diff2', '')
939 939
940 940 if not any((diff1, diff2)):
941 941 h.flash(
942 942 'Need query parameter "diff1" or "diff2" to generate a diff.',
943 943 category='error')
944 944 raise HTTPBadRequest()
945 945
946 946 return redirect(
947 947 url('compare_url', repo_name=repo_name,
948 948 source_ref_type='rev',
949 949 source_ref=diff1,
950 950 target_repo=c.repo_name,
951 951 target_ref_type='rev',
952 952 target_ref=diff2,
953 953 f_path=f_path,
954 954 diffmode='sideside'))
955 955
956 956 def _get_file_node(self, commit_id, f_path):
957 957 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
958 958 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
959 959 try:
960 960 node = commit.get_node(f_path)
961 961 if node.is_dir():
962 962 raise NodeError('%s path is a %s not a file'
963 963 % (node, type(node)))
964 964 except NodeDoesNotExistError:
965 965 commit = EmptyCommit(
966 966 commit_id=commit_id,
967 967 idx=commit.idx,
968 968 repo=commit.repository,
969 969 alias=commit.repository.alias,
970 970 message=commit.message,
971 971 author=commit.author,
972 972 date=commit.date)
973 973 node = FileNode(f_path, '', commit=commit)
974 974 else:
975 975 commit = EmptyCommit(
976 976 repo=c.rhodecode_repo,
977 977 alias=c.rhodecode_repo.alias)
978 978 node = FileNode(f_path, '', commit=commit)
979 979 return node
980 980
981 981 def _get_node_history(self, commit, f_path, commits=None):
982 982 """
983 983 get commit history for given node
984 984
985 985 :param commit: commit to calculate history
986 986 :param f_path: path for node to calculate history for
987 987 :param commits: if passed don't calculate history and take
988 988 commits defined in this list
989 989 """
990 990 # calculate history based on tip
991 991 tip = c.rhodecode_repo.get_commit()
992 992 if commits is None:
993 993 pre_load = ["author", "branch"]
994 994 try:
995 995 commits = tip.get_file_history(f_path, pre_load=pre_load)
996 996 except (NodeDoesNotExistError, CommitError):
997 997 # this node is not present at tip!
998 998 commits = commit.get_file_history(f_path, pre_load=pre_load)
999 999
1000 1000 history = []
1001 1001 commits_group = ([], _("Changesets"))
1002 1002 for commit in commits:
1003 1003 branch = ' (%s)' % commit.branch if commit.branch else ''
1004 1004 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1005 1005 commits_group[0].append((commit.raw_id, n_desc,))
1006 1006 history.append(commits_group)
1007 1007
1008 1008 symbolic_reference = self._symbolic_reference
1009 1009
1010 1010 if c.rhodecode_repo.alias == 'svn':
1011 1011 adjusted_f_path = self._adjust_file_path_for_svn(
1012 1012 f_path, c.rhodecode_repo)
1013 1013 if adjusted_f_path != f_path:
1014 1014 log.debug(
1015 1015 'Recognized svn tag or branch in file "%s", using svn '
1016 1016 'specific symbolic references', f_path)
1017 1017 f_path = adjusted_f_path
1018 1018 symbolic_reference = self._symbolic_reference_svn
1019 1019
1020 1020 branches = self._create_references(
1021 1021 c.rhodecode_repo.branches, symbolic_reference, f_path)
1022 1022 branches_group = (branches, _("Branches"))
1023 1023
1024 1024 tags = self._create_references(
1025 1025 c.rhodecode_repo.tags, symbolic_reference, f_path)
1026 1026 tags_group = (tags, _("Tags"))
1027 1027
1028 1028 history.append(branches_group)
1029 1029 history.append(tags_group)
1030 1030
1031 1031 return history, commits
1032 1032
1033 1033 def _adjust_file_path_for_svn(self, f_path, repo):
1034 1034 """
1035 1035 Computes the relative path of `f_path`.
1036 1036
1037 1037 This is mainly based on prefix matching of the recognized tags and
1038 1038 branches in the underlying repository.
1039 1039 """
1040 1040 tags_and_branches = itertools.chain(
1041 1041 repo.branches.iterkeys(),
1042 1042 repo.tags.iterkeys())
1043 1043 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1044 1044
1045 1045 for name in tags_and_branches:
1046 1046 if f_path.startswith(name + '/'):
1047 1047 f_path = vcspath.relpath(f_path, name)
1048 1048 break
1049 1049 return f_path
1050 1050
1051 1051 def _create_references(
1052 1052 self, branches_or_tags, symbolic_reference, f_path):
1053 1053 items = []
1054 1054 for name, commit_id in branches_or_tags.items():
1055 1055 sym_ref = symbolic_reference(commit_id, name, f_path)
1056 1056 items.append((sym_ref, name))
1057 1057 return items
1058 1058
1059 1059 def _symbolic_reference(self, commit_id, name, f_path):
1060 1060 return commit_id
1061 1061
1062 1062 def _symbolic_reference_svn(self, commit_id, name, f_path):
1063 1063 new_f_path = vcspath.join(name, f_path)
1064 1064 return u'%s@%s' % (new_f_path, commit_id)
1065 1065
1066 1066 @LoginRequired()
1067 1067 @XHRRequired()
1068 1068 @HasRepoPermissionAnyDecorator(
1069 1069 'repository.read', 'repository.write', 'repository.admin')
1070 1070 @jsonify
1071 1071 def nodelist(self, repo_name, revision, f_path):
1072 1072 commit = self.__get_commit_or_redirect(revision, repo_name)
1073 1073
1074 1074 metadata = self._get_nodelist_at_commit(
1075 1075 repo_name, commit.raw_id, f_path)
1076 1076 return {'nodes': metadata}
1077 1077
1078 1078 @LoginRequired()
1079 1079 @XHRRequired()
1080 1080 @HasRepoPermissionAnyDecorator(
1081 1081 'repository.read', 'repository.write', 'repository.admin')
1082 1082 def nodetree_full(self, repo_name, commit_id, f_path):
1083 1083 """
1084 1084 Returns rendered html of file tree that contains commit date,
1085 1085 author, revision for the specified combination of
1086 1086 repo, commit_id and file path
1087 1087
1088 1088 :param repo_name: name of the repository
1089 1089 :param commit_id: commit_id of file tree
1090 1090 :param f_path: file path of the requested directory
1091 1091 """
1092 1092
1093 1093 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1094 1094 try:
1095 1095 dir_node = commit.get_node(f_path)
1096 1096 except RepositoryError as e:
1097 1097 return 'error {}'.format(safe_str(e))
1098 1098
1099 1099 if dir_node.is_file():
1100 1100 return ''
1101 1101
1102 1102 c.file = dir_node
1103 1103 c.commit = commit
1104 1104
1105 1105 # using force=True here, make a little trick. We flush the cache and
1106 1106 # compute it using the same key as without full_load, so the fully
1107 1107 # loaded cached tree is now returned instead of partial
1108 1108 return self._get_tree_at_commit(
1109 1109 repo_name, commit.raw_id, dir_node.path, full_load=True,
1110 1110 force=True)
@@ -1,257 +1,255 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 from rhodecode.model import meta
25 25 from rhodecode.model.db import User, UserLog, Repository
26 26
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30 # action as key, and expected action_data as value
31 31 ACTIONS_V1 = {
32 32 'user.login.success': {'user_agent': ''},
33 33 'user.login.failure': {'user_agent': ''},
34 34 'user.logout': {'user_agent': ''},
35 35 'user.password.reset_request': {},
36 36 'user.push': {'user_agent': '', 'commit_ids': []},
37 37 'user.pull': {'user_agent': ''},
38 38
39 39 'user.create': {'data': {}},
40 40 'user.delete': {'old_data': {}},
41 41 'user.edit': {'old_data': {}},
42 42 'user.edit.permissions': {},
43 43 'user.edit.ip.add': {'ip': {}, 'user': {}},
44 44 'user.edit.ip.delete': {'ip': {}, 'user': {}},
45 45 'user.edit.token.add': {'token': {}, 'user': {}},
46 46 'user.edit.token.delete': {'token': {}, 'user': {}},
47 47 'user.edit.email.add': {'email': ''},
48 48 'user.edit.email.delete': {'email': ''},
49 49 'user.edit.password_reset.enabled': {},
50 50 'user.edit.password_reset.disabled': {},
51 51
52 52 'user_group.create': {'data': {}},
53 53 'user_group.delete': {'old_data': {}},
54 54 'user_group.edit': {'old_data': {}},
55 55 'user_group.edit.permissions': {},
56 'user_group.edit.member.add': {},
57 'user_group.edit.member.delete': {},
56 'user_group.edit.member.add': {'user': {}},
57 'user_group.edit.member.delete': {'user': {}},
58 58
59 59 'repo.create': {'data': {}},
60 60 'repo.fork': {'data': {}},
61 61 'repo.edit': {'old_data': {}},
62 62 'repo.edit.permissions': {},
63 63 'repo.delete': {'old_data': {}},
64 64 'repo.commit.strip': {'commit_id': ''},
65 65 'repo.archive.download': {'user_agent': '', 'archive_name': '',
66 66 'archive_spec': '', 'archive_cached': ''},
67 67 'repo.pull_request.create': '',
68 68 'repo.pull_request.edit': '',
69 69 'repo.pull_request.delete': '',
70 70 'repo.pull_request.close': '',
71 71 'repo.pull_request.merge': '',
72 72 'repo.pull_request.vote': '',
73 73 'repo.pull_request.comment.create': '',
74 74 'repo.pull_request.comment.delete': '',
75 75
76 76 'repo.pull_request.reviewer.add': '',
77 77 'repo.pull_request.reviewer.delete': '',
78 78
79 'repo.commit.comment.create': '',
80 'repo.commit.comment.delete': '',
79 'repo.commit.comment.create': {'data': {}},
80 'repo.commit.comment.delete': {'data': {}},
81 81 'repo.commit.vote': '',
82 82
83 83 'repo_group.create': {'data': {}},
84 84 'repo_group.edit': {'old_data': {}},
85 85 'repo_group.edit.permissions': {},
86 86 'repo_group.delete': {'old_data': {}},
87 87 }
88 88 ACTIONS = ACTIONS_V1
89 89
90 90 SOURCE_WEB = 'source_web'
91 91 SOURCE_API = 'source_api'
92 92
93 93
94 94 class UserWrap(object):
95 95 """
96 96 Fake object used to imitate AuthUser
97 97 """
98 98
99 99 def __init__(self, user_id=None, username=None, ip_addr=None):
100 100 self.user_id = user_id
101 101 self.username = username
102 102 self.ip_addr = ip_addr
103 103
104 104
105 105 class RepoWrap(object):
106 106 """
107 107 Fake object used to imitate RepoObject that audit logger requires
108 108 """
109 109
110 110 def __init__(self, repo_id=None, repo_name=None):
111 111 self.repo_id = repo_id
112 112 self.repo_name = repo_name
113 113
114 114
115 115 def _store_log(action_name, action_data, user_id, username, user_data,
116 116 ip_address, repository_id, repository_name):
117 117 user_log = UserLog()
118 118 user_log.version = UserLog.VERSION_2
119 119
120 120 user_log.action = action_name
121 121 user_log.action_data = action_data
122 122
123 123 user_log.user_ip = ip_address
124 124
125 125 user_log.user_id = user_id
126 126 user_log.username = username
127 127 user_log.user_data = user_data
128 128
129 129 user_log.repository_id = repository_id
130 130 user_log.repository_name = repository_name
131 131
132 132 user_log.action_date = datetime.datetime.now()
133 133
134 134 log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s',
135 135 action_name, user_id, username, ip_address)
136 136
137 137 return user_log
138 138
139 139
140 140 def store_web(*args, **kwargs):
141 141 if 'action_data' not in kwargs:
142 142 kwargs['action_data'] = {}
143 143 kwargs['action_data'].update({
144 144 'source': SOURCE_WEB
145 145 })
146 146 return store(*args, **kwargs)
147 147
148 148
149 149 def store_api(*args, **kwargs):
150 150 if 'action_data' not in kwargs:
151 151 kwargs['action_data'] = {}
152 152 kwargs['action_data'].update({
153 153 'source': SOURCE_API
154 154 })
155 155 return store(*args, **kwargs)
156 156
157 157
158 158 def store(action, user, action_data=None, user_data=None, ip_addr=None,
159 159 repo=None, sa_session=None, commit=False):
160 160 """
161 161 Audit logger for various actions made by users, typically this
162 162 results in a call such::
163 163
164 164 from rhodecode.lib import audit_logger
165 165
166 166 audit_logger.store(
167 action='repo.edit', user=self._rhodecode_user)
167 'repo.edit', user=self._rhodecode_user)
168 168 audit_logger.store(
169 action='repo.delete', action_data={'data': repo_data},
169 'repo.delete', action_data={'data': repo_data},
170 170 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
171 171
172 172 # repo action
173 173 audit_logger.store(
174 action='repo.delete',
174 'repo.delete',
175 175 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
176 176 repo=audit_logger.RepoWrap(repo_name='some-repo'))
177 177
178 178 # repo action, when we know and have the repository object already
179 179 audit_logger.store(
180 action='repo.delete',
181 action_data={'source': audit_logger.SOURCE_WEB, },
180 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
182 181 user=self._rhodecode_user,
183 182 repo=repo_object)
184 183
185 184 # alternative wrapper to the above
186 185 audit_logger.store_web(
187 action='repo.delete',
188 action_data={},
186 'repo.delete', action_data={},
189 187 user=self._rhodecode_user,
190 188 repo=repo_object)
191 189
192 190 # without an user ?
193 191 audit_logger.store(
194 action='user.login.failure',
192 'user.login.failure',
195 193 user=audit_logger.UserWrap(
196 194 username=self.request.params.get('username'),
197 195 ip_addr=self.request.remote_addr))
198 196
199 197 """
200 198 from rhodecode.lib.utils2 import safe_unicode
201 199 from rhodecode.lib.auth import AuthUser
202 200
203 201 action_spec = ACTIONS.get(action, None)
204 202 if action_spec is None:
205 203 raise ValueError('Action `{}` is not supported'.format(action))
206 204
207 205 if not sa_session:
208 206 sa_session = meta.Session()
209 207
210 208 try:
211 209 username = getattr(user, 'username', None)
212 210 if not username:
213 211 pass
214 212
215 213 user_id = getattr(user, 'user_id', None)
216 214 if not user_id:
217 215 # maybe we have username ? Try to figure user_id from username
218 216 if username:
219 217 user_id = getattr(
220 218 User.get_by_username(username), 'user_id', None)
221 219
222 220 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
223 221 if not ip_addr:
224 222 pass
225 223
226 224 if not user_data:
227 225 # try to get this from the auth user
228 226 if isinstance(user, AuthUser):
229 227 user_data = {
230 228 'username': user.username,
231 229 'email': user.email,
232 230 }
233 231
234 232 repository_name = getattr(repo, 'repo_name', None)
235 233 repository_id = getattr(repo, 'repo_id', None)
236 234 if not repository_id:
237 235 # maybe we have repo_name ? Try to figure repo_id from repo_name
238 236 if repository_name:
239 237 repository_id = getattr(
240 238 Repository.get_by_repo_name(repository_name), 'repo_id', None)
241 239
242 240 user_log = _store_log(
243 241 action_name=safe_unicode(action),
244 242 action_data=action_data or {},
245 243 user_id=user_id,
246 244 username=username,
247 245 user_data=user_data or {},
248 246 ip_address=safe_unicode(ip_addr),
249 247 repository_id=repository_id,
250 248 repository_name=repository_name
251 249 )
252 250 sa_session.add(user_log)
253 251 if commit:
254 252 sa_session.commit()
255 253
256 254 except Exception:
257 255 log.exception('AUDIT: failed to store audit log')
@@ -1,299 +1,297 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode task modules, containing all task that suppose to be run
23 23 by celery daemon
24 24 """
25 25
26 26
27 27 import os
28 28 import logging
29 29
30 30 from celery.task import task
31 31 from pylons import config
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.celerylib import (
36 36 run_task, dbsession, __get_lockkey, LockHeld, DaemonLock,
37 37 get_session, vcsconnection, RhodecodeCeleryTask)
38 38 from rhodecode.lib.hooks_base import log_create_repository
39 39 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
40 40 from rhodecode.lib.utils import add_cache
41 41 from rhodecode.lib.utils2 import safe_int, str2bool
42 42 from rhodecode.model.db import Repository, User
43 43
44 44
45 45 add_cache(config) # pragma: no cover
46 46
47 47
48 48 def get_logger(cls):
49 49 if rhodecode.CELERY_ENABLED:
50 50 try:
51 51 log = cls.get_logger()
52 52 except Exception:
53 53 log = logging.getLogger(__name__)
54 54 else:
55 55 log = logging.getLogger(__name__)
56 56
57 57 return log
58 58
59 59
60 60 @task(ignore_result=True, base=RhodecodeCeleryTask)
61 61 @dbsession
62 62 def send_email(recipients, subject, body='', html_body='', email_config=None):
63 63 """
64 64 Sends an email with defined parameters from the .ini files.
65 65
66 66 :param recipients: list of recipients, it this is empty the defined email
67 67 address from field 'email_to' is used instead
68 68 :param subject: subject of the mail
69 69 :param body: body of the mail
70 70 :param html_body: html version of body
71 71 """
72 72 log = get_logger(send_email)
73 73
74 74 email_config = email_config or rhodecode.CONFIG
75 75 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
76 76 if not recipients:
77 77 # if recipients are not defined we send to email_config + all admins
78 78 admins = [
79 79 u.email for u in User.query().filter(User.admin == True).all()]
80 80 recipients = [email_config.get('email_to')] + admins
81 81
82 82 mail_server = email_config.get('smtp_server') or None
83 83 if mail_server is None:
84 84 log.error("SMTP server information missing. Sending email failed. "
85 85 "Make sure that `smtp_server` variable is configured "
86 86 "inside the .ini file")
87 87 return False
88 88
89 89 mail_from = email_config.get('app_email_from', 'RhodeCode')
90 90 user = email_config.get('smtp_username')
91 91 passwd = email_config.get('smtp_password')
92 92 mail_port = email_config.get('smtp_port')
93 93 tls = str2bool(email_config.get('smtp_use_tls'))
94 94 ssl = str2bool(email_config.get('smtp_use_ssl'))
95 95 debug = str2bool(email_config.get('debug'))
96 96 smtp_auth = email_config.get('smtp_auth')
97 97
98 98 try:
99 99 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
100 100 mail_port, ssl, tls, debug=debug)
101 101 m.send(recipients, subject, body, html_body)
102 102 except Exception:
103 103 log.exception('Mail sending failed')
104 104 return False
105 105 return True
106 106
107 107
108 108 @task(ignore_result=True, base=RhodecodeCeleryTask)
109 109 @dbsession
110 110 @vcsconnection
111 111 def create_repo(form_data, cur_user):
112 112 from rhodecode.model.repo import RepoModel
113 113 from rhodecode.model.user import UserModel
114 114 from rhodecode.model.settings import SettingsModel
115 115
116 116 log = get_logger(create_repo)
117 117 DBS = get_session()
118 118
119 119 cur_user = UserModel(DBS)._get_user(cur_user)
120 120 owner = cur_user
121 121
122 122 repo_name = form_data['repo_name']
123 123 repo_name_full = form_data['repo_name_full']
124 124 repo_type = form_data['repo_type']
125 125 description = form_data['repo_description']
126 126 private = form_data['repo_private']
127 127 clone_uri = form_data.get('clone_uri')
128 128 repo_group = safe_int(form_data['repo_group'])
129 129 landing_rev = form_data['repo_landing_rev']
130 130 copy_fork_permissions = form_data.get('copy_permissions')
131 131 copy_group_permissions = form_data.get('repo_copy_permissions')
132 132 fork_of = form_data.get('fork_parent_id')
133 133 state = form_data.get('repo_state', Repository.STATE_PENDING)
134 134
135 135 # repo creation defaults, private and repo_type are filled in form
136 136 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
137 137 enable_statistics = form_data.get(
138 138 'enable_statistics', defs.get('repo_enable_statistics'))
139 139 enable_locking = form_data.get(
140 140 'enable_locking', defs.get('repo_enable_locking'))
141 141 enable_downloads = form_data.get(
142 142 'enable_downloads', defs.get('repo_enable_downloads'))
143 143
144 144 try:
145 145 repo = RepoModel(DBS)._create_repo(
146 146 repo_name=repo_name_full,
147 147 repo_type=repo_type,
148 148 description=description,
149 149 owner=owner,
150 150 private=private,
151 151 clone_uri=clone_uri,
152 152 repo_group=repo_group,
153 153 landing_rev=landing_rev,
154 154 fork_of=fork_of,
155 155 copy_fork_permissions=copy_fork_permissions,
156 156 copy_group_permissions=copy_group_permissions,
157 157 enable_statistics=enable_statistics,
158 158 enable_locking=enable_locking,
159 159 enable_downloads=enable_downloads,
160 160 state=state
161 161 )
162 162 DBS.commit()
163 163
164 164 # now create this repo on Filesystem
165 165 RepoModel(DBS)._create_filesystem_repo(
166 166 repo_name=repo_name,
167 167 repo_type=repo_type,
168 168 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
169 169 clone_uri=clone_uri,
170 170 )
171 171 repo = Repository.get_by_repo_name(repo_name_full)
172 172 log_create_repository(created_by=owner.username, **repo.get_dict())
173 173
174 174 # update repo commit caches initially
175 175 repo.update_commit_cache()
176 176
177 177 # set new created state
178 178 repo.set_state(Repository.STATE_CREATED)
179 179 repo_id = repo.repo_id
180 180 repo_data = repo.get_api_data()
181 181
182 audit_logger.store_web(
183 action='repo.create',
184 action_data={'data': repo_data},
182 audit_logger.store(
183 'repo.create', action_data={'data': repo_data},
185 184 user=cur_user,
186 185 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
187 186
188 187 DBS.commit()
189 188 except Exception:
190 189 log.warning('Exception occurred when creating repository, '
191 190 'doing cleanup...', exc_info=True)
192 191 # rollback things manually !
193 192 repo = Repository.get_by_repo_name(repo_name_full)
194 193 if repo:
195 194 Repository.delete(repo.repo_id)
196 195 DBS.commit()
197 196 RepoModel(DBS)._delete_filesystem_repo(repo)
198 197 raise
199 198
200 199 # it's an odd fix to make celery fail task when exception occurs
201 200 def on_failure(self, *args, **kwargs):
202 201 pass
203 202
204 203 return True
205 204
206 205
207 206 @task(ignore_result=True, base=RhodecodeCeleryTask)
208 207 @dbsession
209 208 @vcsconnection
210 209 def create_repo_fork(form_data, cur_user):
211 210 """
212 211 Creates a fork of repository using internal VCS methods
213 212
214 213 :param form_data:
215 214 :param cur_user:
216 215 """
217 216 from rhodecode.model.repo import RepoModel
218 217 from rhodecode.model.user import UserModel
219 218
220 219 log = get_logger(create_repo_fork)
221 220 DBS = get_session()
222 221
223 222 cur_user = UserModel(DBS)._get_user(cur_user)
224 223 owner = cur_user
225 224
226 225 repo_name = form_data['repo_name'] # fork in this case
227 226 repo_name_full = form_data['repo_name_full']
228 227 repo_type = form_data['repo_type']
229 228 description = form_data['description']
230 229 private = form_data['private']
231 230 clone_uri = form_data.get('clone_uri')
232 231 repo_group = safe_int(form_data['repo_group'])
233 232 landing_rev = form_data['landing_rev']
234 233 copy_fork_permissions = form_data.get('copy_permissions')
235 234 fork_id = safe_int(form_data.get('fork_parent_id'))
236 235
237 236 try:
238 237 fork_of = RepoModel(DBS)._get_repo(fork_id)
239 238 RepoModel(DBS)._create_repo(
240 239 repo_name=repo_name_full,
241 240 repo_type=repo_type,
242 241 description=description,
243 242 owner=owner,
244 243 private=private,
245 244 clone_uri=clone_uri,
246 245 repo_group=repo_group,
247 246 landing_rev=landing_rev,
248 247 fork_of=fork_of,
249 248 copy_fork_permissions=copy_fork_permissions
250 249 )
251 250
252 251 DBS.commit()
253 252
254 253 base_path = Repository.base_path()
255 254 source_repo_path = os.path.join(base_path, fork_of.repo_name)
256 255
257 256 # now create this repo on Filesystem
258 257 RepoModel(DBS)._create_filesystem_repo(
259 258 repo_name=repo_name,
260 259 repo_type=repo_type,
261 260 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
262 261 clone_uri=source_repo_path,
263 262 )
264 263 repo = Repository.get_by_repo_name(repo_name_full)
265 264 log_create_repository(created_by=owner.username, **repo.get_dict())
266 265
267 266 # update repo commit caches initially
268 267 config = repo._config
269 268 config.set('extensions', 'largefiles', '')
270 269 repo.update_commit_cache(config=config)
271 270
272 271 # set new created state
273 272 repo.set_state(Repository.STATE_CREATED)
274 273
275 274 repo_id = repo.repo_id
276 275 repo_data = repo.get_api_data()
277 audit_logger.store_web(
278 action='repo.fork',
279 action_data={'data': repo_data},
276 audit_logger.store(
277 'repo.fork', action_data={'data': repo_data},
280 278 user=cur_user,
281 279 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
282 280
283 281 DBS.commit()
284 282 except Exception as e:
285 283 log.warning('Exception %s occurred when forking repository, '
286 284 'doing cleanup...', e)
287 285 # rollback things manually !
288 286 repo = Repository.get_by_repo_name(repo_name_full)
289 287 if repo:
290 288 Repository.delete(repo.repo_id)
291 289 DBS.commit()
292 290 RepoModel(DBS)._delete_filesystem_repo(repo)
293 291 raise
294 292
295 293 # it's an odd fix to make celery fail task when exception occurs
296 294 def on_failure(self, *args, **kwargs):
297 295 pass
298 296
299 297 return True
@@ -1,425 +1,425 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of hooks run by RhodeCode Enterprise
24 24 """
25 25
26 26 import os
27 27 import collections
28 28 import logging
29 29
30 30 import rhodecode
31 31 from rhodecode import events
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.utils2 import safe_str
35 35 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
36 36 from rhodecode.model.db import Repository, User
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
42 42
43 43
44 44 def is_shadow_repo(extras):
45 45 """
46 46 Returns ``True`` if this is an action executed against a shadow repository.
47 47 """
48 48 return extras['is_shadow_repo']
49 49
50 50
51 51 def _get_scm_size(alias, root_path):
52 52
53 53 if not alias.startswith('.'):
54 54 alias += '.'
55 55
56 56 size_scm, size_root = 0, 0
57 57 for path, unused_dirs, files in os.walk(safe_str(root_path)):
58 58 if path.find(alias) != -1:
59 59 for f in files:
60 60 try:
61 61 size_scm += os.path.getsize(os.path.join(path, f))
62 62 except OSError:
63 63 pass
64 64 else:
65 65 for f in files:
66 66 try:
67 67 size_root += os.path.getsize(os.path.join(path, f))
68 68 except OSError:
69 69 pass
70 70
71 71 size_scm_f = h.format_byte_size_binary(size_scm)
72 72 size_root_f = h.format_byte_size_binary(size_root)
73 73 size_total_f = h.format_byte_size_binary(size_root + size_scm)
74 74
75 75 return size_scm_f, size_root_f, size_total_f
76 76
77 77
78 78 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
79 79 def repo_size(extras):
80 80 """Present size of repository after push."""
81 81 repo = Repository.get_by_repo_name(extras.repository)
82 82 vcs_part = safe_str(u'.%s' % repo.repo_type)
83 83 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
84 84 repo.repo_full_path)
85 85 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
86 86 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
87 87 return HookResponse(0, msg)
88 88
89 89
90 90 def pre_push(extras):
91 91 """
92 92 Hook executed before pushing code.
93 93
94 94 It bans pushing when the repository is locked.
95 95 """
96 96
97 97 usr = User.get_by_username(extras.username)
98 98 output = ''
99 99 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
100 100 locked_by = User.get(extras.locked_by[0]).username
101 101 reason = extras.locked_by[2]
102 102 # this exception is interpreted in git/hg middlewares and based
103 103 # on that proper return code is server to client
104 104 _http_ret = HTTPLockedRC(
105 105 _locked_by_explanation(extras.repository, locked_by, reason))
106 106 if str(_http_ret.code).startswith('2'):
107 107 # 2xx Codes don't raise exceptions
108 108 output = _http_ret.title
109 109 else:
110 110 raise _http_ret
111 111
112 112 # Propagate to external components. This is done after checking the
113 113 # lock, for consistent behavior.
114 114 if not is_shadow_repo(extras):
115 115 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
116 116 events.trigger(events.RepoPrePushEvent(
117 117 repo_name=extras.repository, extras=extras))
118 118
119 119 return HookResponse(0, output)
120 120
121 121
122 122 def pre_pull(extras):
123 123 """
124 124 Hook executed before pulling the code.
125 125
126 126 It bans pulling when the repository is locked.
127 127 """
128 128
129 129 output = ''
130 130 if extras.locked_by[0]:
131 131 locked_by = User.get(extras.locked_by[0]).username
132 132 reason = extras.locked_by[2]
133 133 # this exception is interpreted in git/hg middlewares and based
134 134 # on that proper return code is server to client
135 135 _http_ret = HTTPLockedRC(
136 136 _locked_by_explanation(extras.repository, locked_by, reason))
137 137 if str(_http_ret.code).startswith('2'):
138 138 # 2xx Codes don't raise exceptions
139 139 output = _http_ret.title
140 140 else:
141 141 raise _http_ret
142 142
143 143 # Propagate to external components. This is done after checking the
144 144 # lock, for consistent behavior.
145 145 if not is_shadow_repo(extras):
146 146 pre_pull_extension(**extras)
147 147 events.trigger(events.RepoPrePullEvent(
148 148 repo_name=extras.repository, extras=extras))
149 149
150 150 return HookResponse(0, output)
151 151
152 152
153 153 def post_pull(extras):
154 154 """Hook executed after client pulls the code."""
155 155
156 156 audit_user = audit_logger.UserWrap(
157 157 username=extras.username,
158 158 ip_addr=extras.ip)
159 159 repo = audit_logger.RepoWrap(repo_name=extras.repository)
160 160 audit_logger.store(
161 action='user.pull', action_data={
161 'user.pull', action_data={
162 162 'user_agent': extras.user_agent},
163 163 user=audit_user, repo=repo, commit=True)
164 164
165 165 # Propagate to external components.
166 166 if not is_shadow_repo(extras):
167 167 post_pull_extension(**extras)
168 168 events.trigger(events.RepoPullEvent(
169 169 repo_name=extras.repository, extras=extras))
170 170
171 171 output = ''
172 172 # make lock is a tri state False, True, None. We only make lock on True
173 173 if extras.make_lock is True and not is_shadow_repo(extras):
174 174 user = User.get_by_username(extras.username)
175 175 Repository.lock(Repository.get_by_repo_name(extras.repository),
176 176 user.user_id,
177 177 lock_reason=Repository.LOCK_PULL)
178 178 msg = 'Made lock on repo `%s`' % (extras.repository,)
179 179 output += msg
180 180
181 181 if extras.locked_by[0]:
182 182 locked_by = User.get(extras.locked_by[0]).username
183 183 reason = extras.locked_by[2]
184 184 _http_ret = HTTPLockedRC(
185 185 _locked_by_explanation(extras.repository, locked_by, reason))
186 186 if str(_http_ret.code).startswith('2'):
187 187 # 2xx Codes don't raise exceptions
188 188 output += _http_ret.title
189 189
190 190 return HookResponse(0, output)
191 191
192 192
193 193 def post_push(extras):
194 194 """Hook executed after user pushes to the repository."""
195 195 commit_ids = extras.commit_ids
196 196
197 197 # log the push call
198 198 audit_user = audit_logger.UserWrap(
199 199 username=extras.username, ip_addr=extras.ip)
200 200 repo = audit_logger.RepoWrap(repo_name=extras.repository)
201 201 audit_logger.store(
202 action='user.push', action_data={
202 'user.push', action_data={
203 203 'user_agent': extras.user_agent,
204 204 'commit_ids': commit_ids[:10000]},
205 205 user=audit_user, repo=repo, commit=True)
206 206
207 207 # Propagate to external components.
208 208 if not is_shadow_repo(extras):
209 209 post_push_extension(
210 210 repo_store_path=Repository.base_path(),
211 211 pushed_revs=commit_ids,
212 212 **extras)
213 213 events.trigger(events.RepoPushEvent(
214 214 repo_name=extras.repository,
215 215 pushed_commit_ids=commit_ids,
216 216 extras=extras))
217 217
218 218 output = ''
219 219 # make lock is a tri state False, True, None. We only release lock on False
220 220 if extras.make_lock is False and not is_shadow_repo(extras):
221 221 Repository.unlock(Repository.get_by_repo_name(extras.repository))
222 222 msg = 'Released lock on repo `%s`\n' % extras.repository
223 223 output += msg
224 224
225 225 if extras.locked_by[0]:
226 226 locked_by = User.get(extras.locked_by[0]).username
227 227 reason = extras.locked_by[2]
228 228 _http_ret = HTTPLockedRC(
229 229 _locked_by_explanation(extras.repository, locked_by, reason))
230 230 # TODO: johbo: if not?
231 231 if str(_http_ret.code).startswith('2'):
232 232 # 2xx Codes don't raise exceptions
233 233 output += _http_ret.title
234 234
235 235 if extras.new_refs:
236 236 tmpl = \
237 237 extras.server_url + '/' + \
238 238 extras.repository + \
239 239 "/pull-request/new?{ref_type}={ref_name}"
240 240 for branch_name in extras.new_refs['branches']:
241 241 output += 'RhodeCode: open pull request link: {}\n'.format(
242 242 tmpl.format(ref_type='branch', ref_name=branch_name))
243 243
244 244 for book_name in extras.new_refs['bookmarks']:
245 245 output += 'RhodeCode: open pull request link: {}\n'.format(
246 246 tmpl.format(ref_type='bookmark', ref_name=book_name))
247 247
248 248 output += 'RhodeCode: push completed\n'
249 249 return HookResponse(0, output)
250 250
251 251
252 252 def _locked_by_explanation(repo_name, user_name, reason):
253 253 message = (
254 254 'Repository `%s` locked by user `%s`. Reason:`%s`'
255 255 % (repo_name, user_name, reason))
256 256 return message
257 257
258 258
259 259 def check_allowed_create_user(user_dict, created_by, **kwargs):
260 260 # pre create hooks
261 261 if pre_create_user.is_active():
262 262 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
263 263 if not allowed:
264 264 raise UserCreationError(reason)
265 265
266 266
267 267 class ExtensionCallback(object):
268 268 """
269 269 Forwards a given call to rcextensions, sanitizes keyword arguments.
270 270
271 271 Does check if there is an extension active for that hook. If it is
272 272 there, it will forward all `kwargs_keys` keyword arguments to the
273 273 extension callback.
274 274 """
275 275
276 276 def __init__(self, hook_name, kwargs_keys):
277 277 self._hook_name = hook_name
278 278 self._kwargs_keys = set(kwargs_keys)
279 279
280 280 def __call__(self, *args, **kwargs):
281 281 log.debug('Calling extension callback for %s', self._hook_name)
282 282
283 283 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
284 284 # backward compat for removed api_key for old hooks. THis was it works
285 285 # with older rcextensions that require api_key present
286 286 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
287 287 kwargs_to_pass['api_key'] = '_DEPRECATED_'
288 288
289 289 callback = self._get_callback()
290 290 if callback:
291 291 return callback(**kwargs_to_pass)
292 292 else:
293 293 log.debug('extensions callback not found skipping...')
294 294
295 295 def is_active(self):
296 296 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
297 297
298 298 def _get_callback(self):
299 299 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
300 300
301 301
302 302 pre_pull_extension = ExtensionCallback(
303 303 hook_name='PRE_PULL_HOOK',
304 304 kwargs_keys=(
305 305 'server_url', 'config', 'scm', 'username', 'ip', 'action',
306 306 'repository'))
307 307
308 308
309 309 post_pull_extension = ExtensionCallback(
310 310 hook_name='PULL_HOOK',
311 311 kwargs_keys=(
312 312 'server_url', 'config', 'scm', 'username', 'ip', 'action',
313 313 'repository'))
314 314
315 315
316 316 pre_push_extension = ExtensionCallback(
317 317 hook_name='PRE_PUSH_HOOK',
318 318 kwargs_keys=(
319 319 'server_url', 'config', 'scm', 'username', 'ip', 'action',
320 320 'repository', 'repo_store_path', 'commit_ids'))
321 321
322 322
323 323 post_push_extension = ExtensionCallback(
324 324 hook_name='PUSH_HOOK',
325 325 kwargs_keys=(
326 326 'server_url', 'config', 'scm', 'username', 'ip', 'action',
327 327 'repository', 'repo_store_path', 'pushed_revs'))
328 328
329 329
330 330 pre_create_user = ExtensionCallback(
331 331 hook_name='PRE_CREATE_USER_HOOK',
332 332 kwargs_keys=(
333 333 'username', 'password', 'email', 'firstname', 'lastname', 'active',
334 334 'admin', 'created_by'))
335 335
336 336
337 337 log_create_pull_request = ExtensionCallback(
338 338 hook_name='CREATE_PULL_REQUEST',
339 339 kwargs_keys=(
340 340 'server_url', 'config', 'scm', 'username', 'ip', 'action',
341 341 'repository', 'pull_request_id', 'url', 'title', 'description',
342 342 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
343 343 'mergeable', 'source', 'target', 'author', 'reviewers'))
344 344
345 345
346 346 log_merge_pull_request = ExtensionCallback(
347 347 hook_name='MERGE_PULL_REQUEST',
348 348 kwargs_keys=(
349 349 'server_url', 'config', 'scm', 'username', 'ip', 'action',
350 350 'repository', 'pull_request_id', 'url', 'title', 'description',
351 351 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
352 352 'mergeable', 'source', 'target', 'author', 'reviewers'))
353 353
354 354
355 355 log_close_pull_request = ExtensionCallback(
356 356 hook_name='CLOSE_PULL_REQUEST',
357 357 kwargs_keys=(
358 358 'server_url', 'config', 'scm', 'username', 'ip', 'action',
359 359 'repository', 'pull_request_id', 'url', 'title', 'description',
360 360 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
361 361 'mergeable', 'source', 'target', 'author', 'reviewers'))
362 362
363 363
364 364 log_review_pull_request = ExtensionCallback(
365 365 hook_name='REVIEW_PULL_REQUEST',
366 366 kwargs_keys=(
367 367 'server_url', 'config', 'scm', 'username', 'ip', 'action',
368 368 'repository', 'pull_request_id', 'url', 'title', 'description',
369 369 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
370 370 'mergeable', 'source', 'target', 'author', 'reviewers'))
371 371
372 372
373 373 log_update_pull_request = ExtensionCallback(
374 374 hook_name='UPDATE_PULL_REQUEST',
375 375 kwargs_keys=(
376 376 'server_url', 'config', 'scm', 'username', 'ip', 'action',
377 377 'repository', 'pull_request_id', 'url', 'title', 'description',
378 378 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
379 379 'mergeable', 'source', 'target', 'author', 'reviewers'))
380 380
381 381
382 382 log_create_user = ExtensionCallback(
383 383 hook_name='CREATE_USER_HOOK',
384 384 kwargs_keys=(
385 385 'username', 'full_name_or_username', 'full_contact', 'user_id',
386 386 'name', 'firstname', 'short_contact', 'admin', 'lastname',
387 387 'ip_addresses', 'extern_type', 'extern_name',
388 388 'email', 'api_keys', 'last_login',
389 389 'full_name', 'active', 'password', 'emails',
390 390 'inherit_default_permissions', 'created_by', 'created_on'))
391 391
392 392
393 393 log_delete_user = ExtensionCallback(
394 394 hook_name='DELETE_USER_HOOK',
395 395 kwargs_keys=(
396 396 'username', 'full_name_or_username', 'full_contact', 'user_id',
397 397 'name', 'firstname', 'short_contact', 'admin', 'lastname',
398 398 'ip_addresses',
399 399 'email', 'last_login',
400 400 'full_name', 'active', 'password', 'emails',
401 401 'inherit_default_permissions', 'deleted_by'))
402 402
403 403
404 404 log_create_repository = ExtensionCallback(
405 405 hook_name='CREATE_REPO_HOOK',
406 406 kwargs_keys=(
407 407 'repo_name', 'repo_type', 'description', 'private', 'created_on',
408 408 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
409 409 'clone_uri', 'fork_id', 'group_id', 'created_by'))
410 410
411 411
412 412 log_delete_repository = ExtensionCallback(
413 413 hook_name='DELETE_REPO_HOOK',
414 414 kwargs_keys=(
415 415 'repo_name', 'repo_type', 'description', 'private', 'created_on',
416 416 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
417 417 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
418 418
419 419
420 420 log_create_repository_group = ExtensionCallback(
421 421 hook_name='CREATE_REPO_GROUP_HOOK',
422 422 kwargs_keys=(
423 423 'group_name', 'group_parent_id', 'group_description',
424 424 'group_id', 'user_id', 'created_by', 'created_on',
425 425 'enable_locking'))
@@ -1,618 +1,623 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 user group model for RhodeCode
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.lib.utils2 import safe_str, safe_unicode
31 31 from rhodecode.lib.exceptions import (
32 32 UserGroupAssignedException, RepoGroupAssignmentError)
33 33 from rhodecode.lib.utils2 import (
34 34 get_current_rhodecode_user, action_logger_generic)
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.scm import UserGroupList
37 37 from rhodecode.model.db import (
38 38 true, func, User, UserGroupMember, UserGroup,
39 39 UserGroupRepoToPerm, Permission, UserGroupToPerm, UserUserGroupToPerm,
40 40 UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm)
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class UserGroupModel(BaseModel):
47 47
48 48 cls = UserGroup
49 49
50 50 def _get_user_group(self, user_group):
51 51 return self._get_instance(UserGroup, user_group,
52 52 callback=UserGroup.get_by_group_name)
53 53
54 54 def _create_default_perms(self, user_group):
55 55 # create default permission
56 56 default_perm = 'usergroup.read'
57 57 def_user = User.get_default_user()
58 58 for p in def_user.user_perms:
59 59 if p.permission.permission_name.startswith('usergroup.'):
60 60 default_perm = p.permission.permission_name
61 61 break
62 62
63 63 user_group_to_perm = UserUserGroupToPerm()
64 64 user_group_to_perm.permission = Permission.get_by_key(default_perm)
65 65
66 66 user_group_to_perm.user_group = user_group
67 67 user_group_to_perm.user_id = def_user.user_id
68 68 return user_group_to_perm
69 69
70 70 def update_permissions(self, user_group, perm_additions=None, perm_updates=None,
71 71 perm_deletions=None, check_perms=True, cur_user=None):
72 72 from rhodecode.lib.auth import HasUserGroupPermissionAny
73 73 if not perm_additions:
74 74 perm_additions = []
75 75 if not perm_updates:
76 76 perm_updates = []
77 77 if not perm_deletions:
78 78 perm_deletions = []
79 79
80 80 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
81 81
82 82 # update permissions
83 83 for member_id, perm, member_type in perm_updates:
84 84 member_id = int(member_id)
85 85 if member_type == 'user':
86 86 # this updates existing one
87 87 self.grant_user_permission(
88 88 user_group=user_group, user=member_id, perm=perm
89 89 )
90 90 else:
91 91 # check if we have permissions to alter this usergroup
92 92 member_name = UserGroup.get(member_id).users_group_name
93 93 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
94 94 self.grant_user_group_permission(
95 95 target_user_group=user_group, user_group=member_id, perm=perm
96 96 )
97 97
98 98 # set new permissions
99 99 for member_id, perm, member_type in perm_additions:
100 100 member_id = int(member_id)
101 101 if member_type == 'user':
102 102 self.grant_user_permission(
103 103 user_group=user_group, user=member_id, perm=perm
104 104 )
105 105 else:
106 106 # check if we have permissions to alter this usergroup
107 107 member_name = UserGroup.get(member_id).users_group_name
108 108 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
109 109 self.grant_user_group_permission(
110 110 target_user_group=user_group, user_group=member_id, perm=perm
111 111 )
112 112
113 113 # delete permissions
114 114 for member_id, perm, member_type in perm_deletions:
115 115 member_id = int(member_id)
116 116 if member_type == 'user':
117 117 self.revoke_user_permission(user_group=user_group, user=member_id)
118 118 else:
119 119 # check if we have permissions to alter this usergroup
120 120 member_name = UserGroup.get(member_id).users_group_name
121 121 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
122 122 self.revoke_user_group_permission(
123 123 target_user_group=user_group, user_group=member_id
124 124 )
125 125
126 126 def get(self, user_group_id, cache=False):
127 127 return UserGroup.get(user_group_id)
128 128
129 129 def get_group(self, user_group):
130 130 return self._get_user_group(user_group)
131 131
132 132 def get_by_name(self, name, cache=False, case_insensitive=False):
133 133 return UserGroup.get_by_group_name(name, cache, case_insensitive)
134 134
135 135 def create(self, name, description, owner, active=True, group_data=None):
136 136 try:
137 137 new_user_group = UserGroup()
138 138 new_user_group.user = self._get_user(owner)
139 139 new_user_group.users_group_name = name
140 140 new_user_group.user_group_description = description
141 141 new_user_group.users_group_active = active
142 142 if group_data:
143 143 new_user_group.group_data = group_data
144 144 self.sa.add(new_user_group)
145 145 perm_obj = self._create_default_perms(new_user_group)
146 146 self.sa.add(perm_obj)
147 147
148 148 self.grant_user_permission(user_group=new_user_group,
149 149 user=owner, perm='usergroup.admin')
150 150
151 151 return new_user_group
152 152 except Exception:
153 153 log.error(traceback.format_exc())
154 154 raise
155 155
156 156 def _get_memberships_for_user_ids(self, user_group, user_id_list):
157 157 members = []
158 158 for user_id in user_id_list:
159 159 member = self._get_membership(user_group.users_group_id, user_id)
160 160 members.append(member)
161 161 return members
162 162
163 163 def _get_added_and_removed_user_ids(self, user_group, user_id_list):
164 164 current_members = user_group.members or []
165 165 current_members_ids = [m.user.user_id for m in current_members]
166 166
167 167 added_members = [
168 168 user_id for user_id in user_id_list
169 169 if user_id not in current_members_ids]
170 170 if user_id_list == []:
171 171 # all members were deleted
172 172 deleted_members = current_members_ids
173 173 else:
174 174 deleted_members = [
175 175 user_id for user_id in current_members_ids
176 176 if user_id not in user_id_list]
177 177
178 return (added_members, deleted_members)
178 return added_members, deleted_members
179 179
180 180 def _set_users_as_members(self, user_group, user_ids):
181 181 user_group.members = []
182 182 self.sa.flush()
183 183 members = self._get_memberships_for_user_ids(
184 184 user_group, user_ids)
185 185 user_group.members = members
186 186 self.sa.add(user_group)
187 187
188 188 def _update_members_from_user_ids(self, user_group, user_ids):
189 189 added, removed = self._get_added_and_removed_user_ids(
190 190 user_group, user_ids)
191 191 self._set_users_as_members(user_group, user_ids)
192 192 self._log_user_changes('added to', user_group, added)
193 193 self._log_user_changes('removed from', user_group, removed)
194 return added, removed
194 195
195 196 def _clean_members_data(self, members_data):
196 197 if not members_data:
197 198 members_data = []
198 199
199 200 members = []
200 201 for user in members_data:
201 202 uid = int(user['member_user_id'])
202 203 if uid not in members and user['type'] in ['new', 'existing']:
203 204 members.append(uid)
204 205 return members
205 206
206 207 def update(self, user_group, form_data):
207 208 user_group = self._get_user_group(user_group)
208 209 if 'users_group_name' in form_data:
209 210 user_group.users_group_name = form_data['users_group_name']
210 211 if 'users_group_active' in form_data:
211 212 user_group.users_group_active = form_data['users_group_active']
212 213 if 'user_group_description' in form_data:
213 214 user_group.user_group_description = form_data[
214 215 'user_group_description']
215 216
216 217 # handle owner change
217 218 if 'user' in form_data:
218 219 owner = form_data['user']
219 220 if isinstance(owner, basestring):
220 221 owner = User.get_by_username(form_data['user'])
221 222
222 223 if not isinstance(owner, User):
223 224 raise ValueError(
224 225 'invalid owner for user group: %s' % form_data['user'])
225 226
226 227 user_group.user = owner
227 228
229 added_user_ids = []
230 removed_user_ids = []
228 231 if 'users_group_members' in form_data:
229 232 members_id_list = self._clean_members_data(
230 233 form_data['users_group_members'])
231 self._update_members_from_user_ids(user_group, members_id_list)
234 added_user_ids, removed_user_ids = \
235 self._update_members_from_user_ids(user_group, members_id_list)
232 236
233 237 self.sa.add(user_group)
238 return user_group, added_user_ids, removed_user_ids
234 239
235 240 def delete(self, user_group, force=False):
236 241 """
237 242 Deletes repository group, unless force flag is used
238 243 raises exception if there are members in that group, else deletes
239 244 group and users
240 245
241 246 :param user_group:
242 247 :param force:
243 248 """
244 249 user_group = self._get_user_group(user_group)
245 250 try:
246 251 # check if this group is not assigned to repo
247 252 assigned_to_repo = [x.repository for x in UserGroupRepoToPerm.query()\
248 253 .filter(UserGroupRepoToPerm.users_group == user_group).all()]
249 254 # check if this group is not assigned to repo
250 255 assigned_to_repo_group = [x.group for x in UserGroupRepoGroupToPerm.query()\
251 256 .filter(UserGroupRepoGroupToPerm.users_group == user_group).all()]
252 257
253 258 if (assigned_to_repo or assigned_to_repo_group) and not force:
254 259 assigned = ','.join(map(safe_str,
255 260 assigned_to_repo+assigned_to_repo_group))
256 261
257 262 raise UserGroupAssignedException(
258 263 'UserGroup assigned to %s' % (assigned,))
259 264 self.sa.delete(user_group)
260 265 except Exception:
261 266 log.error(traceback.format_exc())
262 267 raise
263 268
264 269 def _log_user_changes(self, action, user_group, user_or_users):
265 270 users = user_or_users
266 271 if not isinstance(users, (list, tuple)):
267 272 users = [users]
268 273 rhodecode_user = get_current_rhodecode_user()
269 274 ipaddr = getattr(rhodecode_user, 'ip_addr', '')
270 275 group_name = user_group.users_group_name
271 276
272 277 for user_or_user_id in users:
273 278 user = self._get_user(user_or_user_id)
274 279 log_text = 'User {user} {action} {group}'.format(
275 280 action=action, user=user.username, group=group_name)
276 281 log.info('Logging action: {0} by {1} ip:{2}'.format(
277 282 log_text, rhodecode_user, ipaddr))
278 283
279 284 def _find_user_in_group(self, user, user_group):
280 285 user_group_member = None
281 286 for m in user_group.members:
282 287 if m.user_id == user.user_id:
283 288 # Found this user's membership row
284 289 user_group_member = m
285 290 break
286 291
287 292 return user_group_member
288 293
289 294 def _get_membership(self, user_group_id, user_id):
290 295 user_group_member = UserGroupMember(user_group_id, user_id)
291 296 return user_group_member
292 297
293 298 def add_user_to_group(self, user_group, user):
294 299 user_group = self._get_user_group(user_group)
295 300 user = self._get_user(user)
296 301 user_member = self._find_user_in_group(user, user_group)
297 302 if user_member:
298 303 # user already in the group, skip
299 304 return True
300 305
301 306 member = self._get_membership(
302 307 user_group.users_group_id, user.user_id)
303 308 user_group.members.append(member)
304 309
305 310 try:
306 311 self.sa.add(member)
307 312 except Exception:
308 313 # what could go wrong here?
309 314 log.error(traceback.format_exc())
310 315 raise
311 316
312 317 self._log_user_changes('added to', user_group, user)
313 318 return member
314 319
315 320 def remove_user_from_group(self, user_group, user):
316 321 user_group = self._get_user_group(user_group)
317 322 user = self._get_user(user)
318 323 user_group_member = self._find_user_in_group(user, user_group)
319 324
320 325 if not user_group_member:
321 326 # User isn't in that group
322 327 return False
323 328
324 329 try:
325 330 self.sa.delete(user_group_member)
326 331 except Exception:
327 332 log.error(traceback.format_exc())
328 333 raise
329 334
330 335 self._log_user_changes('removed from', user_group, user)
331 336 return True
332 337
333 338 def has_perm(self, user_group, perm):
334 339 user_group = self._get_user_group(user_group)
335 340 perm = self._get_perm(perm)
336 341
337 342 return UserGroupToPerm.query()\
338 343 .filter(UserGroupToPerm.users_group == user_group)\
339 344 .filter(UserGroupToPerm.permission == perm).scalar() is not None
340 345
341 346 def grant_perm(self, user_group, perm):
342 347 user_group = self._get_user_group(user_group)
343 348 perm = self._get_perm(perm)
344 349
345 350 # if this permission is already granted skip it
346 351 _perm = UserGroupToPerm.query()\
347 352 .filter(UserGroupToPerm.users_group == user_group)\
348 353 .filter(UserGroupToPerm.permission == perm)\
349 354 .scalar()
350 355 if _perm:
351 356 return
352 357
353 358 new = UserGroupToPerm()
354 359 new.users_group = user_group
355 360 new.permission = perm
356 361 self.sa.add(new)
357 362 return new
358 363
359 364 def revoke_perm(self, user_group, perm):
360 365 user_group = self._get_user_group(user_group)
361 366 perm = self._get_perm(perm)
362 367
363 368 obj = UserGroupToPerm.query()\
364 369 .filter(UserGroupToPerm.users_group == user_group)\
365 370 .filter(UserGroupToPerm.permission == perm).scalar()
366 371 if obj:
367 372 self.sa.delete(obj)
368 373
369 374 def grant_user_permission(self, user_group, user, perm):
370 375 """
371 376 Grant permission for user on given user group, or update
372 377 existing one if found
373 378
374 379 :param user_group: Instance of UserGroup, users_group_id,
375 380 or users_group_name
376 381 :param user: Instance of User, user_id or username
377 382 :param perm: Instance of Permission, or permission_name
378 383 """
379 384
380 385 user_group = self._get_user_group(user_group)
381 386 user = self._get_user(user)
382 387 permission = self._get_perm(perm)
383 388
384 389 # check if we have that permission already
385 390 obj = self.sa.query(UserUserGroupToPerm)\
386 391 .filter(UserUserGroupToPerm.user == user)\
387 392 .filter(UserUserGroupToPerm.user_group == user_group)\
388 393 .scalar()
389 394 if obj is None:
390 395 # create new !
391 396 obj = UserUserGroupToPerm()
392 397 obj.user_group = user_group
393 398 obj.user = user
394 399 obj.permission = permission
395 400 self.sa.add(obj)
396 401 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
397 402 action_logger_generic(
398 403 'granted permission: {} to user: {} on usergroup: {}'.format(
399 404 perm, user, user_group), namespace='security.usergroup')
400 405
401 406 return obj
402 407
403 408 def revoke_user_permission(self, user_group, user):
404 409 """
405 410 Revoke permission for user on given user group
406 411
407 412 :param user_group: Instance of UserGroup, users_group_id,
408 413 or users_group name
409 414 :param user: Instance of User, user_id or username
410 415 """
411 416
412 417 user_group = self._get_user_group(user_group)
413 418 user = self._get_user(user)
414 419
415 420 obj = self.sa.query(UserUserGroupToPerm)\
416 421 .filter(UserUserGroupToPerm.user == user)\
417 422 .filter(UserUserGroupToPerm.user_group == user_group)\
418 423 .scalar()
419 424 if obj:
420 425 self.sa.delete(obj)
421 426 log.debug('Revoked perm on %s on %s', user_group, user)
422 427 action_logger_generic(
423 428 'revoked permission from user: {} on usergroup: {}'.format(
424 429 user, user_group), namespace='security.usergroup')
425 430
426 431 def grant_user_group_permission(self, target_user_group, user_group, perm):
427 432 """
428 433 Grant user group permission for given target_user_group
429 434
430 435 :param target_user_group:
431 436 :param user_group:
432 437 :param perm:
433 438 """
434 439 target_user_group = self._get_user_group(target_user_group)
435 440 user_group = self._get_user_group(user_group)
436 441 permission = self._get_perm(perm)
437 442 # forbid assigning same user group to itself
438 443 if target_user_group == user_group:
439 444 raise RepoGroupAssignmentError('target repo:%s cannot be '
440 445 'assigned to itself' % target_user_group)
441 446
442 447 # check if we have that permission already
443 448 obj = self.sa.query(UserGroupUserGroupToPerm)\
444 449 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
445 450 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
446 451 .scalar()
447 452 if obj is None:
448 453 # create new !
449 454 obj = UserGroupUserGroupToPerm()
450 455 obj.user_group = user_group
451 456 obj.target_user_group = target_user_group
452 457 obj.permission = permission
453 458 self.sa.add(obj)
454 459 log.debug(
455 460 'Granted perm %s to %s on %s', perm, target_user_group, user_group)
456 461 action_logger_generic(
457 462 'granted permission: {} to usergroup: {} on usergroup: {}'.format(
458 463 perm, user_group, target_user_group),
459 464 namespace='security.usergroup')
460 465
461 466 return obj
462 467
463 468 def revoke_user_group_permission(self, target_user_group, user_group):
464 469 """
465 470 Revoke user group permission for given target_user_group
466 471
467 472 :param target_user_group:
468 473 :param user_group:
469 474 """
470 475 target_user_group = self._get_user_group(target_user_group)
471 476 user_group = self._get_user_group(user_group)
472 477
473 478 obj = self.sa.query(UserGroupUserGroupToPerm)\
474 479 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
475 480 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
476 481 .scalar()
477 482 if obj:
478 483 self.sa.delete(obj)
479 484 log.debug(
480 485 'Revoked perm on %s on %s', target_user_group, user_group)
481 486 action_logger_generic(
482 487 'revoked permission from usergroup: {} on usergroup: {}'.format(
483 488 user_group, target_user_group),
484 489 namespace='security.repogroup')
485 490
486 491 def enforce_groups(self, user, groups, extern_type=None):
487 492 user = self._get_user(user)
488 493 log.debug('Enforcing groups %s on user %s', groups, user)
489 494 current_groups = user.group_member
490 495 # find the external created groups
491 496 externals = [x.users_group for x in current_groups
492 497 if 'extern_type' in x.users_group.group_data]
493 498
494 499 # calculate from what groups user should be removed
495 500 # externals that are not in groups
496 501 for gr in externals:
497 502 if gr.users_group_name not in groups:
498 503 log.debug('Removing user %s from user group %s', user, gr)
499 504 self.remove_user_from_group(gr, user)
500 505
501 506 # now we calculate in which groups user should be == groups params
502 507 owner = User.get_first_super_admin().username
503 508 for gr in set(groups):
504 509 existing_group = UserGroup.get_by_group_name(gr)
505 510 if not existing_group:
506 511 desc = 'Automatically created from plugin:%s' % extern_type
507 512 # we use first admin account to set the owner of the group
508 513 existing_group = UserGroupModel().create(
509 514 gr, desc, owner, group_data={'extern_type': extern_type})
510 515
511 516 # we can only add users to special groups created via plugins
512 517 managed = 'extern_type' in existing_group.group_data
513 518 if managed:
514 519 log.debug('Adding user %s to user group %s', user, gr)
515 520 UserGroupModel().add_user_to_group(existing_group, user)
516 521 else:
517 522 log.debug('Skipping addition to group %s since it is '
518 523 'not set to be automatically synchronized' % gr)
519 524
520 525 def change_groups(self, user, groups):
521 526 """
522 527 This method changes user group assignment
523 528 :param user: User
524 529 :param groups: array of UserGroupModel
525 530 :return:
526 531 """
527 532 user = self._get_user(user)
528 533 log.debug('Changing user(%s) assignment to groups(%s)', user, groups)
529 534 current_groups = user.group_member
530 535 current_groups = [x.users_group for x in current_groups]
531 536
532 537 # calculate from what groups user should be removed/add
533 538 groups = set(groups)
534 539 current_groups = set(current_groups)
535 540
536 541 groups_to_remove = current_groups - groups
537 542 groups_to_add = groups - current_groups
538 543
539 544 for gr in groups_to_remove:
540 545 log.debug('Removing user %s from user group %s', user.username, gr.users_group_name)
541 546 self.remove_user_from_group(gr.users_group_name, user.username)
542 547 for gr in groups_to_add:
543 548 log.debug('Adding user %s to user group %s', user.username, gr.users_group_name)
544 549 UserGroupModel().add_user_to_group(gr.users_group_name, user.username)
545 550
546 551 def _serialize_user_group(self, user_group):
547 552 import rhodecode.lib.helpers as h
548 553 return {
549 554 'id': user_group.users_group_id,
550 555 # TODO: marcink figure out a way to generate the url for the
551 556 # icon
552 557 'icon_link': '',
553 558 'value_display': 'Group: %s (%d members)' % (
554 559 user_group.users_group_name, len(user_group.members),),
555 560 'value': user_group.users_group_name,
556 561 'description': user_group.user_group_description,
557 562 'owner': user_group.user.username,
558 563
559 564 'owner_icon': h.gravatar_url(user_group.user.email, 30),
560 565 'value_display_owner': h.person(user_group.user.email),
561 566
562 567 'value_type': 'user_group',
563 568 'active': user_group.users_group_active,
564 569 }
565 570
566 571 def get_user_groups(self, name_contains=None, limit=20, only_active=True,
567 572 expand_groups=False):
568 573 query = self.sa.query(UserGroup)
569 574 if only_active:
570 575 query = query.filter(UserGroup.users_group_active == true())
571 576
572 577 if name_contains:
573 578 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
574 579 query = query.filter(
575 580 UserGroup.users_group_name.ilike(ilike_expression))\
576 581 .order_by(func.length(UserGroup.users_group_name))\
577 582 .order_by(UserGroup.users_group_name)
578 583
579 584 query = query.limit(limit)
580 585 user_groups = query.all()
581 586 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
582 587 user_groups = UserGroupList(user_groups, perm_set=perm_set)
583 588
584 589 # store same serialize method to extract data from User
585 590 from rhodecode.model.user import UserModel
586 591 serialize_user = UserModel()._serialize_user
587 592
588 593 _groups = []
589 594 for group in user_groups:
590 595 entry = self._serialize_user_group(group)
591 596 if expand_groups:
592 597 expanded_members = []
593 598 for member in group.members:
594 599 expanded_members.append(serialize_user(member.user))
595 600 entry['members'] = expanded_members
596 601 _groups.append(entry)
597 602 return _groups
598 603
599 604 @staticmethod
600 605 def get_user_groups_as_dict(user_group):
601 606 import rhodecode.lib.helpers as h
602 607
603 608 data = {
604 609 'users_group_id': user_group.users_group_id,
605 610 'group_name': user_group.users_group_name,
606 611 'group_description': user_group.user_group_description,
607 612 'active': user_group.users_group_active,
608 613 "owner": user_group.user.username,
609 614 'owner_icon': h.gravatar_url(user_group.user.email, 30),
610 615 "owner_data": {
611 616 'owner': user_group.user.username,
612 617 'owner_icon': h.gravatar_url(user_group.user.email, 30)}
613 618 }
614 619 return data
615 620
616 621
617 622
618 623
General Comments 0
You need to be logged in to leave comments. Login now