##// END OF EJS Templates
audit-logs: use specific web/api calls....
marcink -
r1806:99a19fa8 default
parent child Browse files
Show More
@@ -1,2063 +1,2062 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 920 schema = repo_schema.RepoSchema().bind(
921 921 repo_type_options=rhodecode.BACKENDS.keys(),
922 922 repo_ref_options=ref_choices,
923 923 # user caller
924 924 user=apiuser,
925 925 old_values=repo.get_api_data())
926 926 try:
927 927 schema_data = schema.deserialize(dict(
928 928 # we save old value, users cannot change type
929 929 repo_type=repo.repo_type,
930 930
931 931 repo_name=updates['repo_name'],
932 932 repo_owner=updates['user'],
933 933 repo_description=updates['repo_description'],
934 934 repo_clone_uri=updates['clone_uri'],
935 935 repo_fork_of=updates['fork_id'],
936 936 repo_private=updates['repo_private'],
937 937 repo_landing_commit_ref=updates['repo_landing_rev'],
938 938 repo_enable_statistics=updates['repo_enable_statistics'],
939 939 repo_enable_downloads=updates['repo_enable_downloads'],
940 940 repo_enable_locking=updates['repo_enable_locking']))
941 941 except validation_schema.Invalid as err:
942 942 raise JSONRPCValidationError(colander_exc=err)
943 943
944 944 # save validated data back into the updates dict
945 945 validated_updates = dict(
946 946 repo_name=schema_data['repo_group']['repo_name_without_group'],
947 947 repo_group=schema_data['repo_group']['repo_group_id'],
948 948
949 949 user=schema_data['repo_owner'],
950 950 repo_description=schema_data['repo_description'],
951 951 repo_private=schema_data['repo_private'],
952 952 clone_uri=schema_data['repo_clone_uri'],
953 953 repo_landing_rev=schema_data['repo_landing_commit_ref'],
954 954 repo_enable_statistics=schema_data['repo_enable_statistics'],
955 955 repo_enable_locking=schema_data['repo_enable_locking'],
956 956 repo_enable_downloads=schema_data['repo_enable_downloads'],
957 957 )
958 958
959 959 if schema_data['repo_fork_of']:
960 960 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
961 961 validated_updates['fork_id'] = fork_repo.repo_id
962 962
963 963 # extra fields
964 964 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
965 965 if fields:
966 966 validated_updates.update(fields)
967 967
968 968 try:
969 969 RepoModel().update(repo, **validated_updates)
970 970 Session().commit()
971 971 return {
972 972 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
973 973 'repository': repo.get_api_data(include_secrets=include_secrets)
974 974 }
975 975 except Exception:
976 976 log.exception(
977 977 u"Exception while trying to update the repository %s",
978 978 repoid)
979 979 raise JSONRPCError('failed to update repo `%s`' % repoid)
980 980
981 981
982 982 @jsonrpc_method()
983 983 def fork_repo(request, apiuser, repoid, fork_name,
984 984 owner=Optional(OAttr('apiuser')),
985 985 description=Optional(''),
986 986 private=Optional(False),
987 987 clone_uri=Optional(None),
988 988 landing_rev=Optional('rev:tip'),
989 989 copy_permissions=Optional(False)):
990 990 """
991 991 Creates a fork of the specified |repo|.
992 992
993 993 * If the fork_name contains "/", fork will be created inside
994 994 a repository group or nested repository groups
995 995
996 996 For example "foo/bar/fork-repo" will create fork called "fork-repo"
997 997 inside group "foo/bar". You have to have permissions to access and
998 998 write to the last repository group ("bar" in this example)
999 999
1000 1000 This command can only be run using an |authtoken| with minimum
1001 1001 read permissions of the forked repo, create fork permissions for an user.
1002 1002
1003 1003 :param apiuser: This is filled automatically from the |authtoken|.
1004 1004 :type apiuser: AuthUser
1005 1005 :param repoid: Set repository name or repository ID.
1006 1006 :type repoid: str or int
1007 1007 :param fork_name: Set the fork name, including it's repository group membership.
1008 1008 :type fork_name: str
1009 1009 :param owner: Set the fork owner.
1010 1010 :type owner: str
1011 1011 :param description: Set the fork description.
1012 1012 :type description: str
1013 1013 :param copy_permissions: Copy permissions from parent |repo|. The
1014 1014 default is False.
1015 1015 :type copy_permissions: bool
1016 1016 :param private: Make the fork private. The default is False.
1017 1017 :type private: bool
1018 1018 :param landing_rev: Set the landing revision. The default is tip.
1019 1019
1020 1020 Example output:
1021 1021
1022 1022 .. code-block:: bash
1023 1023
1024 1024 id : <id_for_response>
1025 1025 api_key : "<api_key>"
1026 1026 args: {
1027 1027 "repoid" : "<reponame or repo_id>",
1028 1028 "fork_name": "<forkname>",
1029 1029 "owner": "<username or user_id = Optional(=apiuser)>",
1030 1030 "description": "<description>",
1031 1031 "copy_permissions": "<bool>",
1032 1032 "private": "<bool>",
1033 1033 "landing_rev": "<landing_rev>"
1034 1034 }
1035 1035
1036 1036 Example error output:
1037 1037
1038 1038 .. code-block:: bash
1039 1039
1040 1040 id : <id_given_in_input>
1041 1041 result: {
1042 1042 "msg": "Created fork of `<reponame>` as `<forkname>`",
1043 1043 "success": true,
1044 1044 "task": "<celery task id or None if done sync>"
1045 1045 }
1046 1046 error: null
1047 1047
1048 1048 """
1049 1049
1050 1050 repo = get_repo_or_error(repoid)
1051 1051 repo_name = repo.repo_name
1052 1052
1053 1053 if not has_superadmin_permission(apiuser):
1054 1054 # check if we have at least read permission for
1055 1055 # this repo that we fork !
1056 1056 _perms = (
1057 1057 'repository.admin', 'repository.write', 'repository.read')
1058 1058 validate_repo_permissions(apiuser, repoid, repo, _perms)
1059 1059
1060 1060 # check if the regular user has at least fork permissions as well
1061 1061 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1062 1062 raise JSONRPCForbidden()
1063 1063
1064 1064 # check if user can set owner parameter
1065 1065 owner = validate_set_owner_permissions(apiuser, owner)
1066 1066
1067 1067 description = Optional.extract(description)
1068 1068 copy_permissions = Optional.extract(copy_permissions)
1069 1069 clone_uri = Optional.extract(clone_uri)
1070 1070 landing_commit_ref = Optional.extract(landing_rev)
1071 1071 private = Optional.extract(private)
1072 1072
1073 1073 schema = repo_schema.RepoSchema().bind(
1074 1074 repo_type_options=rhodecode.BACKENDS.keys(),
1075 1075 # user caller
1076 1076 user=apiuser)
1077 1077
1078 1078 try:
1079 1079 schema_data = schema.deserialize(dict(
1080 1080 repo_name=fork_name,
1081 1081 repo_type=repo.repo_type,
1082 1082 repo_owner=owner.username,
1083 1083 repo_description=description,
1084 1084 repo_landing_commit_ref=landing_commit_ref,
1085 1085 repo_clone_uri=clone_uri,
1086 1086 repo_private=private,
1087 1087 repo_copy_permissions=copy_permissions))
1088 1088 except validation_schema.Invalid as err:
1089 1089 raise JSONRPCValidationError(colander_exc=err)
1090 1090
1091 1091 try:
1092 1092 data = {
1093 1093 'fork_parent_id': repo.repo_id,
1094 1094
1095 1095 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1096 1096 'repo_name_full': schema_data['repo_name'],
1097 1097 'repo_group': schema_data['repo_group']['repo_group_id'],
1098 1098 'repo_type': schema_data['repo_type'],
1099 1099 'description': schema_data['repo_description'],
1100 1100 'private': schema_data['repo_private'],
1101 1101 'copy_permissions': schema_data['repo_copy_permissions'],
1102 1102 'landing_rev': schema_data['repo_landing_commit_ref'],
1103 1103 }
1104 1104
1105 1105 task = RepoModel().create_fork(data, cur_user=owner)
1106 1106 # no commit, it's done in RepoModel, or async via celery
1107 1107 from celery.result import BaseAsyncResult
1108 1108 task_id = None
1109 1109 if isinstance(task, BaseAsyncResult):
1110 1110 task_id = task.task_id
1111 1111 return {
1112 1112 'msg': 'Created fork of `%s` as `%s`' % (
1113 1113 repo.repo_name, schema_data['repo_name']),
1114 1114 'success': True, # cannot return the repo data here since fork
1115 1115 # can be done async
1116 1116 'task': task_id
1117 1117 }
1118 1118 except Exception:
1119 1119 log.exception(
1120 1120 u"Exception while trying to create fork %s",
1121 1121 schema_data['repo_name'])
1122 1122 raise JSONRPCError(
1123 1123 'failed to fork repository `%s` as `%s`' % (
1124 1124 repo_name, schema_data['repo_name']))
1125 1125
1126 1126
1127 1127 @jsonrpc_method()
1128 1128 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1129 1129 """
1130 1130 Deletes a repository.
1131 1131
1132 1132 * When the `forks` parameter is set it's possible to detach or delete
1133 1133 forks of deleted repository.
1134 1134
1135 1135 This command can only be run using an |authtoken| with admin
1136 1136 permissions on the |repo|.
1137 1137
1138 1138 :param apiuser: This is filled automatically from the |authtoken|.
1139 1139 :type apiuser: AuthUser
1140 1140 :param repoid: Set the repository name or repository ID.
1141 1141 :type repoid: str or int
1142 1142 :param forks: Set to `detach` or `delete` forks from the |repo|.
1143 1143 :type forks: Optional(str)
1144 1144
1145 1145 Example error output:
1146 1146
1147 1147 .. code-block:: bash
1148 1148
1149 1149 id : <id_given_in_input>
1150 1150 result: {
1151 1151 "msg": "Deleted repository `<reponame>`",
1152 1152 "success": true
1153 1153 }
1154 1154 error: null
1155 1155 """
1156 1156
1157 1157 repo = get_repo_or_error(repoid)
1158 1158 repo_name = repo.repo_name
1159 1159 if not has_superadmin_permission(apiuser):
1160 1160 _perms = ('repository.admin',)
1161 1161 validate_repo_permissions(apiuser, repoid, repo, _perms)
1162 1162
1163 1163 try:
1164 1164 handle_forks = Optional.extract(forks)
1165 1165 _forks_msg = ''
1166 1166 _forks = [f for f in repo.forks]
1167 1167 if handle_forks == 'detach':
1168 1168 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1169 1169 elif handle_forks == 'delete':
1170 1170 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1171 1171 elif _forks:
1172 1172 raise JSONRPCError(
1173 1173 'Cannot delete `%s` it still contains attached forks' %
1174 1174 (repo.repo_name,)
1175 1175 )
1176 1176 repo_data = repo.get_api_data()
1177 1177 RepoModel().delete(repo, forks=forks)
1178 1178
1179 1179 repo = audit_logger.RepoWrap(repo_id=None,
1180 1180 repo_name=repo.repo_name)
1181 1181
1182 audit_logger.store(
1182 audit_logger.store_api(
1183 1183 action='repo.delete',
1184 action_data={'repo_data': repo_data,
1185 'source': audit_logger.SOURCE_API},
1186 user=apiuser, repo=repo, commit=False)
1184 action_data={'data': repo_data},
1185 user=apiuser, repo=repo)
1187 1186
1188 1187 ScmModel().mark_for_invalidation(repo_name, delete=True)
1189 1188 Session().commit()
1190 1189 return {
1191 1190 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1192 1191 'success': True
1193 1192 }
1194 1193 except Exception:
1195 1194 log.exception("Exception occurred while trying to delete repo")
1196 1195 raise JSONRPCError(
1197 1196 'failed to delete repository `%s`' % (repo_name,)
1198 1197 )
1199 1198
1200 1199
1201 1200 #TODO: marcink, change name ?
1202 1201 @jsonrpc_method()
1203 1202 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1204 1203 """
1205 1204 Invalidates the cache for the specified repository.
1206 1205
1207 1206 This command can only be run using an |authtoken| with admin rights to
1208 1207 the specified repository.
1209 1208
1210 1209 This command takes the following options:
1211 1210
1212 1211 :param apiuser: This is filled automatically from |authtoken|.
1213 1212 :type apiuser: AuthUser
1214 1213 :param repoid: Sets the repository name or repository ID.
1215 1214 :type repoid: str or int
1216 1215 :param delete_keys: This deletes the invalidated keys instead of
1217 1216 just flagging them.
1218 1217 :type delete_keys: Optional(``True`` | ``False``)
1219 1218
1220 1219 Example output:
1221 1220
1222 1221 .. code-block:: bash
1223 1222
1224 1223 id : <id_given_in_input>
1225 1224 result : {
1226 1225 'msg': Cache for repository `<repository name>` was invalidated,
1227 1226 'repository': <repository name>
1228 1227 }
1229 1228 error : null
1230 1229
1231 1230 Example error output:
1232 1231
1233 1232 .. code-block:: bash
1234 1233
1235 1234 id : <id_given_in_input>
1236 1235 result : null
1237 1236 error : {
1238 1237 'Error occurred during cache invalidation action'
1239 1238 }
1240 1239
1241 1240 """
1242 1241
1243 1242 repo = get_repo_or_error(repoid)
1244 1243 if not has_superadmin_permission(apiuser):
1245 1244 _perms = ('repository.admin', 'repository.write',)
1246 1245 validate_repo_permissions(apiuser, repoid, repo, _perms)
1247 1246
1248 1247 delete = Optional.extract(delete_keys)
1249 1248 try:
1250 1249 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1251 1250 return {
1252 1251 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1253 1252 'repository': repo.repo_name
1254 1253 }
1255 1254 except Exception:
1256 1255 log.exception(
1257 1256 "Exception occurred while trying to invalidate repo cache")
1258 1257 raise JSONRPCError(
1259 1258 'Error occurred during cache invalidation action'
1260 1259 )
1261 1260
1262 1261
1263 1262 #TODO: marcink, change name ?
1264 1263 @jsonrpc_method()
1265 1264 def lock(request, apiuser, repoid, locked=Optional(None),
1266 1265 userid=Optional(OAttr('apiuser'))):
1267 1266 """
1268 1267 Sets the lock state of the specified |repo| by the given user.
1269 1268 From more information, see :ref:`repo-locking`.
1270 1269
1271 1270 * If the ``userid`` option is not set, the repository is locked to the
1272 1271 user who called the method.
1273 1272 * If the ``locked`` parameter is not set, the current lock state of the
1274 1273 repository is displayed.
1275 1274
1276 1275 This command can only be run using an |authtoken| with admin rights to
1277 1276 the specified repository.
1278 1277
1279 1278 This command takes the following options:
1280 1279
1281 1280 :param apiuser: This is filled automatically from the |authtoken|.
1282 1281 :type apiuser: AuthUser
1283 1282 :param repoid: Sets the repository name or repository ID.
1284 1283 :type repoid: str or int
1285 1284 :param locked: Sets the lock state.
1286 1285 :type locked: Optional(``True`` | ``False``)
1287 1286 :param userid: Set the repository lock to this user.
1288 1287 :type userid: Optional(str or int)
1289 1288
1290 1289 Example error output:
1291 1290
1292 1291 .. code-block:: bash
1293 1292
1294 1293 id : <id_given_in_input>
1295 1294 result : {
1296 1295 'repo': '<reponame>',
1297 1296 'locked': <bool: lock state>,
1298 1297 'locked_since': <int: lock timestamp>,
1299 1298 'locked_by': <username of person who made the lock>,
1300 1299 'lock_reason': <str: reason for locking>,
1301 1300 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1302 1301 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1303 1302 or
1304 1303 'msg': 'Repo `<repository name>` not locked.'
1305 1304 or
1306 1305 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1307 1306 }
1308 1307 error : null
1309 1308
1310 1309 Example error output:
1311 1310
1312 1311 .. code-block:: bash
1313 1312
1314 1313 id : <id_given_in_input>
1315 1314 result : null
1316 1315 error : {
1317 1316 'Error occurred locking repository `<reponame>`'
1318 1317 }
1319 1318 """
1320 1319
1321 1320 repo = get_repo_or_error(repoid)
1322 1321 if not has_superadmin_permission(apiuser):
1323 1322 # check if we have at least write permission for this repo !
1324 1323 _perms = ('repository.admin', 'repository.write',)
1325 1324 validate_repo_permissions(apiuser, repoid, repo, _perms)
1326 1325
1327 1326 # make sure normal user does not pass someone else userid,
1328 1327 # he is not allowed to do that
1329 1328 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1330 1329 raise JSONRPCError('userid is not the same as your user')
1331 1330
1332 1331 if isinstance(userid, Optional):
1333 1332 userid = apiuser.user_id
1334 1333
1335 1334 user = get_user_or_error(userid)
1336 1335
1337 1336 if isinstance(locked, Optional):
1338 1337 lockobj = repo.locked
1339 1338
1340 1339 if lockobj[0] is None:
1341 1340 _d = {
1342 1341 'repo': repo.repo_name,
1343 1342 'locked': False,
1344 1343 'locked_since': None,
1345 1344 'locked_by': None,
1346 1345 'lock_reason': None,
1347 1346 'lock_state_changed': False,
1348 1347 'msg': 'Repo `%s` not locked.' % repo.repo_name
1349 1348 }
1350 1349 return _d
1351 1350 else:
1352 1351 _user_id, _time, _reason = lockobj
1353 1352 lock_user = get_user_or_error(userid)
1354 1353 _d = {
1355 1354 'repo': repo.repo_name,
1356 1355 'locked': True,
1357 1356 'locked_since': _time,
1358 1357 'locked_by': lock_user.username,
1359 1358 'lock_reason': _reason,
1360 1359 'lock_state_changed': False,
1361 1360 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1362 1361 % (repo.repo_name, lock_user.username,
1363 1362 json.dumps(time_to_datetime(_time))))
1364 1363 }
1365 1364 return _d
1366 1365
1367 1366 # force locked state through a flag
1368 1367 else:
1369 1368 locked = str2bool(locked)
1370 1369 lock_reason = Repository.LOCK_API
1371 1370 try:
1372 1371 if locked:
1373 1372 lock_time = time.time()
1374 1373 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1375 1374 else:
1376 1375 lock_time = None
1377 1376 Repository.unlock(repo)
1378 1377 _d = {
1379 1378 'repo': repo.repo_name,
1380 1379 'locked': locked,
1381 1380 'locked_since': lock_time,
1382 1381 'locked_by': user.username,
1383 1382 'lock_reason': lock_reason,
1384 1383 'lock_state_changed': True,
1385 1384 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1386 1385 % (user.username, repo.repo_name, locked))
1387 1386 }
1388 1387 return _d
1389 1388 except Exception:
1390 1389 log.exception(
1391 1390 "Exception occurred while trying to lock repository")
1392 1391 raise JSONRPCError(
1393 1392 'Error occurred locking repository `%s`' % repo.repo_name
1394 1393 )
1395 1394
1396 1395
1397 1396 @jsonrpc_method()
1398 1397 def comment_commit(
1399 1398 request, apiuser, repoid, commit_id, message, status=Optional(None),
1400 1399 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1401 1400 resolves_comment_id=Optional(None),
1402 1401 userid=Optional(OAttr('apiuser'))):
1403 1402 """
1404 1403 Set a commit comment, and optionally change the status of the commit.
1405 1404
1406 1405 :param apiuser: This is filled automatically from the |authtoken|.
1407 1406 :type apiuser: AuthUser
1408 1407 :param repoid: Set the repository name or repository ID.
1409 1408 :type repoid: str or int
1410 1409 :param commit_id: Specify the commit_id for which to set a comment.
1411 1410 :type commit_id: str
1412 1411 :param message: The comment text.
1413 1412 :type message: str
1414 1413 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1415 1414 'approved', 'rejected', 'under_review'
1416 1415 :type status: str
1417 1416 :param comment_type: Comment type, one of: 'note', 'todo'
1418 1417 :type comment_type: Optional(str), default: 'note'
1419 1418 :param userid: Set the user name of the comment creator.
1420 1419 :type userid: Optional(str or int)
1421 1420
1422 1421 Example error output:
1423 1422
1424 1423 .. code-block:: bash
1425 1424
1426 1425 {
1427 1426 "id" : <id_given_in_input>,
1428 1427 "result" : {
1429 1428 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1430 1429 "status_change": null or <status>,
1431 1430 "success": true
1432 1431 },
1433 1432 "error" : null
1434 1433 }
1435 1434
1436 1435 """
1437 1436 repo = get_repo_or_error(repoid)
1438 1437 if not has_superadmin_permission(apiuser):
1439 1438 _perms = ('repository.read', 'repository.write', 'repository.admin')
1440 1439 validate_repo_permissions(apiuser, repoid, repo, _perms)
1441 1440
1442 1441 try:
1443 1442 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1444 1443 except Exception as e:
1445 1444 log.exception('Failed to fetch commit')
1446 1445 raise JSONRPCError(e.message)
1447 1446
1448 1447 if isinstance(userid, Optional):
1449 1448 userid = apiuser.user_id
1450 1449
1451 1450 user = get_user_or_error(userid)
1452 1451 status = Optional.extract(status)
1453 1452 comment_type = Optional.extract(comment_type)
1454 1453 resolves_comment_id = Optional.extract(resolves_comment_id)
1455 1454
1456 1455 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1457 1456 if status and status not in allowed_statuses:
1458 1457 raise JSONRPCError('Bad status, must be on '
1459 1458 'of %s got %s' % (allowed_statuses, status,))
1460 1459
1461 1460 if resolves_comment_id:
1462 1461 comment = ChangesetComment.get(resolves_comment_id)
1463 1462 if not comment:
1464 1463 raise JSONRPCError(
1465 1464 'Invalid resolves_comment_id `%s` for this commit.'
1466 1465 % resolves_comment_id)
1467 1466 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1468 1467 raise JSONRPCError(
1469 1468 'Comment `%s` is wrong type for setting status to resolved.'
1470 1469 % resolves_comment_id)
1471 1470
1472 1471 try:
1473 1472 rc_config = SettingsModel().get_all_settings()
1474 1473 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1475 1474 status_change_label = ChangesetStatus.get_status_lbl(status)
1476 1475 comm = CommentsModel().create(
1477 1476 message, repo, user, commit_id=commit_id,
1478 1477 status_change=status_change_label,
1479 1478 status_change_type=status,
1480 1479 renderer=renderer,
1481 1480 comment_type=comment_type,
1482 1481 resolves_comment_id=resolves_comment_id
1483 1482 )
1484 1483 if status:
1485 1484 # also do a status change
1486 1485 try:
1487 1486 ChangesetStatusModel().set_status(
1488 1487 repo, status, user, comm, revision=commit_id,
1489 1488 dont_allow_on_closed_pull_request=True
1490 1489 )
1491 1490 except StatusChangeOnClosedPullRequestError:
1492 1491 log.exception(
1493 1492 "Exception occurred while trying to change repo commit status")
1494 1493 msg = ('Changing status on a changeset associated with '
1495 1494 'a closed pull request is not allowed')
1496 1495 raise JSONRPCError(msg)
1497 1496
1498 1497 Session().commit()
1499 1498 return {
1500 1499 'msg': (
1501 1500 'Commented on commit `%s` for repository `%s`' % (
1502 1501 comm.revision, repo.repo_name)),
1503 1502 'status_change': status,
1504 1503 'success': True,
1505 1504 }
1506 1505 except JSONRPCError:
1507 1506 # catch any inside errors, and re-raise them to prevent from
1508 1507 # below global catch to silence them
1509 1508 raise
1510 1509 except Exception:
1511 1510 log.exception("Exception occurred while trying to comment on commit")
1512 1511 raise JSONRPCError(
1513 1512 'failed to set comment on repository `%s`' % (repo.repo_name,)
1514 1513 )
1515 1514
1516 1515
1517 1516 @jsonrpc_method()
1518 1517 def grant_user_permission(request, apiuser, repoid, userid, perm):
1519 1518 """
1520 1519 Grant permissions for the specified user on the given repository,
1521 1520 or update existing permissions if found.
1522 1521
1523 1522 This command can only be run using an |authtoken| with admin
1524 1523 permissions on the |repo|.
1525 1524
1526 1525 :param apiuser: This is filled automatically from the |authtoken|.
1527 1526 :type apiuser: AuthUser
1528 1527 :param repoid: Set the repository name or repository ID.
1529 1528 :type repoid: str or int
1530 1529 :param userid: Set the user name.
1531 1530 :type userid: str
1532 1531 :param perm: Set the user permissions, using the following format
1533 1532 ``(repository.(none|read|write|admin))``
1534 1533 :type perm: str
1535 1534
1536 1535 Example output:
1537 1536
1538 1537 .. code-block:: bash
1539 1538
1540 1539 id : <id_given_in_input>
1541 1540 result: {
1542 1541 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1543 1542 "success": true
1544 1543 }
1545 1544 error: null
1546 1545 """
1547 1546
1548 1547 repo = get_repo_or_error(repoid)
1549 1548 user = get_user_or_error(userid)
1550 1549 perm = get_perm_or_error(perm)
1551 1550 if not has_superadmin_permission(apiuser):
1552 1551 _perms = ('repository.admin',)
1553 1552 validate_repo_permissions(apiuser, repoid, repo, _perms)
1554 1553
1555 1554 try:
1556 1555
1557 1556 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1558 1557
1559 1558 Session().commit()
1560 1559 return {
1561 1560 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1562 1561 perm.permission_name, user.username, repo.repo_name
1563 1562 ),
1564 1563 'success': True
1565 1564 }
1566 1565 except Exception:
1567 1566 log.exception(
1568 1567 "Exception occurred while trying edit permissions for repo")
1569 1568 raise JSONRPCError(
1570 1569 'failed to edit permission for user: `%s` in repo: `%s`' % (
1571 1570 userid, repoid
1572 1571 )
1573 1572 )
1574 1573
1575 1574
1576 1575 @jsonrpc_method()
1577 1576 def revoke_user_permission(request, apiuser, repoid, userid):
1578 1577 """
1579 1578 Revoke permission for a user on the specified repository.
1580 1579
1581 1580 This command can only be run using an |authtoken| with admin
1582 1581 permissions on the |repo|.
1583 1582
1584 1583 :param apiuser: This is filled automatically from the |authtoken|.
1585 1584 :type apiuser: AuthUser
1586 1585 :param repoid: Set the repository name or repository ID.
1587 1586 :type repoid: str or int
1588 1587 :param userid: Set the user name of revoked user.
1589 1588 :type userid: str or int
1590 1589
1591 1590 Example error output:
1592 1591
1593 1592 .. code-block:: bash
1594 1593
1595 1594 id : <id_given_in_input>
1596 1595 result: {
1597 1596 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1598 1597 "success": true
1599 1598 }
1600 1599 error: null
1601 1600 """
1602 1601
1603 1602 repo = get_repo_or_error(repoid)
1604 1603 user = get_user_or_error(userid)
1605 1604 if not has_superadmin_permission(apiuser):
1606 1605 _perms = ('repository.admin',)
1607 1606 validate_repo_permissions(apiuser, repoid, repo, _perms)
1608 1607
1609 1608 try:
1610 1609 RepoModel().revoke_user_permission(repo=repo, user=user)
1611 1610 Session().commit()
1612 1611 return {
1613 1612 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1614 1613 user.username, repo.repo_name
1615 1614 ),
1616 1615 'success': True
1617 1616 }
1618 1617 except Exception:
1619 1618 log.exception(
1620 1619 "Exception occurred while trying revoke permissions to repo")
1621 1620 raise JSONRPCError(
1622 1621 'failed to edit permission for user: `%s` in repo: `%s`' % (
1623 1622 userid, repoid
1624 1623 )
1625 1624 )
1626 1625
1627 1626
1628 1627 @jsonrpc_method()
1629 1628 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1630 1629 """
1631 1630 Grant permission for a user group on the specified repository,
1632 1631 or update existing permissions.
1633 1632
1634 1633 This command can only be run using an |authtoken| with admin
1635 1634 permissions on the |repo|.
1636 1635
1637 1636 :param apiuser: This is filled automatically from the |authtoken|.
1638 1637 :type apiuser: AuthUser
1639 1638 :param repoid: Set the repository name or repository ID.
1640 1639 :type repoid: str or int
1641 1640 :param usergroupid: Specify the ID of the user group.
1642 1641 :type usergroupid: str or int
1643 1642 :param perm: Set the user group permissions using the following
1644 1643 format: (repository.(none|read|write|admin))
1645 1644 :type perm: str
1646 1645
1647 1646 Example output:
1648 1647
1649 1648 .. code-block:: bash
1650 1649
1651 1650 id : <id_given_in_input>
1652 1651 result : {
1653 1652 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1654 1653 "success": true
1655 1654
1656 1655 }
1657 1656 error : null
1658 1657
1659 1658 Example error output:
1660 1659
1661 1660 .. code-block:: bash
1662 1661
1663 1662 id : <id_given_in_input>
1664 1663 result : null
1665 1664 error : {
1666 1665 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1667 1666 }
1668 1667
1669 1668 """
1670 1669
1671 1670 repo = get_repo_or_error(repoid)
1672 1671 perm = get_perm_or_error(perm)
1673 1672 if not has_superadmin_permission(apiuser):
1674 1673 _perms = ('repository.admin',)
1675 1674 validate_repo_permissions(apiuser, repoid, repo, _perms)
1676 1675
1677 1676 user_group = get_user_group_or_error(usergroupid)
1678 1677 if not has_superadmin_permission(apiuser):
1679 1678 # check if we have at least read permission for this user group !
1680 1679 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1681 1680 if not HasUserGroupPermissionAnyApi(*_perms)(
1682 1681 user=apiuser, user_group_name=user_group.users_group_name):
1683 1682 raise JSONRPCError(
1684 1683 'user group `%s` does not exist' % (usergroupid,))
1685 1684
1686 1685 try:
1687 1686 RepoModel().grant_user_group_permission(
1688 1687 repo=repo, group_name=user_group, perm=perm)
1689 1688
1690 1689 Session().commit()
1691 1690 return {
1692 1691 'msg': 'Granted perm: `%s` for user group: `%s` in '
1693 1692 'repo: `%s`' % (
1694 1693 perm.permission_name, user_group.users_group_name,
1695 1694 repo.repo_name
1696 1695 ),
1697 1696 'success': True
1698 1697 }
1699 1698 except Exception:
1700 1699 log.exception(
1701 1700 "Exception occurred while trying change permission on repo")
1702 1701 raise JSONRPCError(
1703 1702 'failed to edit permission for user group: `%s` in '
1704 1703 'repo: `%s`' % (
1705 1704 usergroupid, repo.repo_name
1706 1705 )
1707 1706 )
1708 1707
1709 1708
1710 1709 @jsonrpc_method()
1711 1710 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1712 1711 """
1713 1712 Revoke the permissions of a user group on a given repository.
1714 1713
1715 1714 This command can only be run using an |authtoken| with admin
1716 1715 permissions on the |repo|.
1717 1716
1718 1717 :param apiuser: This is filled automatically from the |authtoken|.
1719 1718 :type apiuser: AuthUser
1720 1719 :param repoid: Set the repository name or repository ID.
1721 1720 :type repoid: str or int
1722 1721 :param usergroupid: Specify the user group ID.
1723 1722 :type usergroupid: str or int
1724 1723
1725 1724 Example output:
1726 1725
1727 1726 .. code-block:: bash
1728 1727
1729 1728 id : <id_given_in_input>
1730 1729 result: {
1731 1730 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1732 1731 "success": true
1733 1732 }
1734 1733 error: null
1735 1734 """
1736 1735
1737 1736 repo = get_repo_or_error(repoid)
1738 1737 if not has_superadmin_permission(apiuser):
1739 1738 _perms = ('repository.admin',)
1740 1739 validate_repo_permissions(apiuser, repoid, repo, _perms)
1741 1740
1742 1741 user_group = get_user_group_or_error(usergroupid)
1743 1742 if not has_superadmin_permission(apiuser):
1744 1743 # check if we have at least read permission for this user group !
1745 1744 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1746 1745 if not HasUserGroupPermissionAnyApi(*_perms)(
1747 1746 user=apiuser, user_group_name=user_group.users_group_name):
1748 1747 raise JSONRPCError(
1749 1748 'user group `%s` does not exist' % (usergroupid,))
1750 1749
1751 1750 try:
1752 1751 RepoModel().revoke_user_group_permission(
1753 1752 repo=repo, group_name=user_group)
1754 1753
1755 1754 Session().commit()
1756 1755 return {
1757 1756 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1758 1757 user_group.users_group_name, repo.repo_name
1759 1758 ),
1760 1759 'success': True
1761 1760 }
1762 1761 except Exception:
1763 1762 log.exception("Exception occurred while trying revoke "
1764 1763 "user group permission on repo")
1765 1764 raise JSONRPCError(
1766 1765 'failed to edit permission for user group: `%s` in '
1767 1766 'repo: `%s`' % (
1768 1767 user_group.users_group_name, repo.repo_name
1769 1768 )
1770 1769 )
1771 1770
1772 1771
1773 1772 @jsonrpc_method()
1774 1773 def pull(request, apiuser, repoid):
1775 1774 """
1776 1775 Triggers a pull on the given repository from a remote location. You
1777 1776 can use this to keep remote repositories up-to-date.
1778 1777
1779 1778 This command can only be run using an |authtoken| with admin
1780 1779 rights to the specified repository. For more information,
1781 1780 see :ref:`config-token-ref`.
1782 1781
1783 1782 This command takes the following options:
1784 1783
1785 1784 :param apiuser: This is filled automatically from the |authtoken|.
1786 1785 :type apiuser: AuthUser
1787 1786 :param repoid: The repository name or repository ID.
1788 1787 :type repoid: str or int
1789 1788
1790 1789 Example output:
1791 1790
1792 1791 .. code-block:: bash
1793 1792
1794 1793 id : <id_given_in_input>
1795 1794 result : {
1796 1795 "msg": "Pulled from `<repository name>`"
1797 1796 "repository": "<repository name>"
1798 1797 }
1799 1798 error : null
1800 1799
1801 1800 Example error output:
1802 1801
1803 1802 .. code-block:: bash
1804 1803
1805 1804 id : <id_given_in_input>
1806 1805 result : null
1807 1806 error : {
1808 1807 "Unable to pull changes from `<reponame>`"
1809 1808 }
1810 1809
1811 1810 """
1812 1811
1813 1812 repo = get_repo_or_error(repoid)
1814 1813 if not has_superadmin_permission(apiuser):
1815 1814 _perms = ('repository.admin',)
1816 1815 validate_repo_permissions(apiuser, repoid, repo, _perms)
1817 1816
1818 1817 try:
1819 1818 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1820 1819 return {
1821 1820 'msg': 'Pulled from `%s`' % repo.repo_name,
1822 1821 'repository': repo.repo_name
1823 1822 }
1824 1823 except Exception:
1825 1824 log.exception("Exception occurred while trying to "
1826 1825 "pull changes from remote location")
1827 1826 raise JSONRPCError(
1828 1827 'Unable to pull changes from `%s`' % repo.repo_name
1829 1828 )
1830 1829
1831 1830
1832 1831 @jsonrpc_method()
1833 1832 def strip(request, apiuser, repoid, revision, branch):
1834 1833 """
1835 1834 Strips the given revision from the specified repository.
1836 1835
1837 1836 * This will remove the revision and all of its decendants.
1838 1837
1839 1838 This command can only be run using an |authtoken| with admin rights to
1840 1839 the specified repository.
1841 1840
1842 1841 This command takes the following options:
1843 1842
1844 1843 :param apiuser: This is filled automatically from the |authtoken|.
1845 1844 :type apiuser: AuthUser
1846 1845 :param repoid: The repository name or repository ID.
1847 1846 :type repoid: str or int
1848 1847 :param revision: The revision you wish to strip.
1849 1848 :type revision: str
1850 1849 :param branch: The branch from which to strip the revision.
1851 1850 :type branch: str
1852 1851
1853 1852 Example output:
1854 1853
1855 1854 .. code-block:: bash
1856 1855
1857 1856 id : <id_given_in_input>
1858 1857 result : {
1859 1858 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1860 1859 "repository": "<repository name>"
1861 1860 }
1862 1861 error : null
1863 1862
1864 1863 Example error output:
1865 1864
1866 1865 .. code-block:: bash
1867 1866
1868 1867 id : <id_given_in_input>
1869 1868 result : null
1870 1869 error : {
1871 1870 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1872 1871 }
1873 1872
1874 1873 """
1875 1874
1876 1875 repo = get_repo_or_error(repoid)
1877 1876 if not has_superadmin_permission(apiuser):
1878 1877 _perms = ('repository.admin',)
1879 1878 validate_repo_permissions(apiuser, repoid, repo, _perms)
1880 1879
1881 1880 try:
1882 1881 ScmModel().strip(repo, revision, branch)
1883 1882 return {
1884 1883 'msg': 'Stripped commit %s from repo `%s`' % (
1885 1884 revision, repo.repo_name),
1886 1885 'repository': repo.repo_name
1887 1886 }
1888 1887 except Exception:
1889 1888 log.exception("Exception while trying to strip")
1890 1889 raise JSONRPCError(
1891 1890 'Unable to strip commit %s from repo `%s`' % (
1892 1891 revision, repo.repo_name)
1893 1892 )
1894 1893
1895 1894
1896 1895 @jsonrpc_method()
1897 1896 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1898 1897 """
1899 1898 Returns all settings for a repository. If key is given it only returns the
1900 1899 setting identified by the key or null.
1901 1900
1902 1901 :param apiuser: This is filled automatically from the |authtoken|.
1903 1902 :type apiuser: AuthUser
1904 1903 :param repoid: The repository name or repository id.
1905 1904 :type repoid: str or int
1906 1905 :param key: Key of the setting to return.
1907 1906 :type: key: Optional(str)
1908 1907
1909 1908 Example output:
1910 1909
1911 1910 .. code-block:: bash
1912 1911
1913 1912 {
1914 1913 "error": null,
1915 1914 "id": 237,
1916 1915 "result": {
1917 1916 "extensions_largefiles": true,
1918 1917 "extensions_evolve": true,
1919 1918 "hooks_changegroup_push_logger": true,
1920 1919 "hooks_changegroup_repo_size": false,
1921 1920 "hooks_outgoing_pull_logger": true,
1922 1921 "phases_publish": "True",
1923 1922 "rhodecode_hg_use_rebase_for_merging": true,
1924 1923 "rhodecode_pr_merge_enabled": true,
1925 1924 "rhodecode_use_outdated_comments": true
1926 1925 }
1927 1926 }
1928 1927 """
1929 1928
1930 1929 # Restrict access to this api method to admins only.
1931 1930 if not has_superadmin_permission(apiuser):
1932 1931 raise JSONRPCForbidden()
1933 1932
1934 1933 try:
1935 1934 repo = get_repo_or_error(repoid)
1936 1935 settings_model = VcsSettingsModel(repo=repo)
1937 1936 settings = settings_model.get_global_settings()
1938 1937 settings.update(settings_model.get_repo_settings())
1939 1938
1940 1939 # If only a single setting is requested fetch it from all settings.
1941 1940 key = Optional.extract(key)
1942 1941 if key is not None:
1943 1942 settings = settings.get(key, None)
1944 1943 except Exception:
1945 1944 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1946 1945 log.exception(msg)
1947 1946 raise JSONRPCError(msg)
1948 1947
1949 1948 return settings
1950 1949
1951 1950
1952 1951 @jsonrpc_method()
1953 1952 def set_repo_settings(request, apiuser, repoid, settings):
1954 1953 """
1955 1954 Update repository settings. Returns true on success.
1956 1955
1957 1956 :param apiuser: This is filled automatically from the |authtoken|.
1958 1957 :type apiuser: AuthUser
1959 1958 :param repoid: The repository name or repository id.
1960 1959 :type repoid: str or int
1961 1960 :param settings: The new settings for the repository.
1962 1961 :type: settings: dict
1963 1962
1964 1963 Example output:
1965 1964
1966 1965 .. code-block:: bash
1967 1966
1968 1967 {
1969 1968 "error": null,
1970 1969 "id": 237,
1971 1970 "result": true
1972 1971 }
1973 1972 """
1974 1973 # Restrict access to this api method to admins only.
1975 1974 if not has_superadmin_permission(apiuser):
1976 1975 raise JSONRPCForbidden()
1977 1976
1978 1977 if type(settings) is not dict:
1979 1978 raise JSONRPCError('Settings have to be a JSON Object.')
1980 1979
1981 1980 try:
1982 1981 settings_model = VcsSettingsModel(repo=repoid)
1983 1982
1984 1983 # Merge global, repo and incoming settings.
1985 1984 new_settings = settings_model.get_global_settings()
1986 1985 new_settings.update(settings_model.get_repo_settings())
1987 1986 new_settings.update(settings)
1988 1987
1989 1988 # Update the settings.
1990 1989 inherit_global_settings = new_settings.get(
1991 1990 'inherit_global_settings', False)
1992 1991 settings_model.create_or_update_repo_settings(
1993 1992 new_settings, inherit_global_settings=inherit_global_settings)
1994 1993 Session().commit()
1995 1994 except Exception:
1996 1995 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1997 1996 log.exception(msg)
1998 1997 raise JSONRPCError(msg)
1999 1998
2000 1999 # Indicate success.
2001 2000 return True
2002 2001
2003 2002
2004 2003 @jsonrpc_method()
2005 2004 def maintenance(request, apiuser, repoid):
2006 2005 """
2007 2006 Triggers a maintenance on the given repository.
2008 2007
2009 2008 This command can only be run using an |authtoken| with admin
2010 2009 rights to the specified repository. For more information,
2011 2010 see :ref:`config-token-ref`.
2012 2011
2013 2012 This command takes the following options:
2014 2013
2015 2014 :param apiuser: This is filled automatically from the |authtoken|.
2016 2015 :type apiuser: AuthUser
2017 2016 :param repoid: The repository name or repository ID.
2018 2017 :type repoid: str or int
2019 2018
2020 2019 Example output:
2021 2020
2022 2021 .. code-block:: bash
2023 2022
2024 2023 id : <id_given_in_input>
2025 2024 result : {
2026 2025 "msg": "executed maintenance command",
2027 2026 "executed_actions": [
2028 2027 <action_message>, <action_message2>...
2029 2028 ],
2030 2029 "repository": "<repository name>"
2031 2030 }
2032 2031 error : null
2033 2032
2034 2033 Example error output:
2035 2034
2036 2035 .. code-block:: bash
2037 2036
2038 2037 id : <id_given_in_input>
2039 2038 result : null
2040 2039 error : {
2041 2040 "Unable to execute maintenance on `<reponame>`"
2042 2041 }
2043 2042
2044 2043 """
2045 2044
2046 2045 repo = get_repo_or_error(repoid)
2047 2046 if not has_superadmin_permission(apiuser):
2048 2047 _perms = ('repository.admin',)
2049 2048 validate_repo_permissions(apiuser, repoid, repo, _perms)
2050 2049
2051 2050 try:
2052 2051 maintenance = repo_maintenance.RepoMaintenance()
2053 2052 executed_actions = maintenance.execute(repo)
2054 2053
2055 2054 return {
2056 2055 'msg': 'executed maintenance command',
2057 2056 'executed_actions': executed_actions,
2058 2057 'repository': repo.repo_name
2059 2058 }
2060 2059 except Exception:
2061 2060 log.exception("Exception occurred while trying to run maintenance")
2062 2061 raise JSONRPCError(
2063 2062 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,425 +1,426 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 audit_logger.store(
175 audit_logger.store_web(
176 176 action='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 audit_logger.store(
194 audit_logger.store_web(
195 195 action='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 audit_logger.store(
214 audit_logger.store_web(
215 215 action='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 audit_logger.store(action='user.password.reset_request',
367 audit_logger.store_web(
368 action='user.password.reset_request',
368 369 action_data=action_data,
369 370 user=self._rhodecode_user, commit=True)
370 371 return HTTPFound(self.request.route_path('reset_password'))
371 372
372 373 except formencode.Invalid as errors:
373 374 render_ctx.update({
374 375 'defaults': errors.value,
375 376 'errors': errors.error_dict,
376 377 })
377 378 if not self.request.params.get('email'):
378 379 # case of empty email, we want to report that
379 380 return render_ctx
380 381
381 382 if 'recaptcha_field' in errors.error_dict:
382 383 # case of failed captcha
383 384 return render_ctx
384 385
385 386 log.debug('faking response on invalid password reset')
386 387 # make this take 2s, to prevent brute forcing.
387 388 time.sleep(2)
388 389 self.session.flash(msg, queue='success')
389 390 return HTTPFound(self.request.route_path('reset_password'))
390 391
391 392 return render_ctx
392 393
393 394 @view_config(route_name='reset_password_confirmation',
394 395 request_method='GET')
395 396 def password_reset_confirmation(self):
396 397
397 398 if self.request.GET and self.request.GET.get('key'):
398 399 # make this take 2s, to prevent brute forcing.
399 400 time.sleep(2)
400 401
401 402 token = AuthTokenModel().get_auth_token(
402 403 self.request.GET.get('key'))
403 404
404 405 # verify token is the correct role
405 406 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
406 407 log.debug('Got token with role:%s expected is %s',
407 408 getattr(token, 'role', 'EMPTY_TOKEN'),
408 409 UserApiKeys.ROLE_PASSWORD_RESET)
409 410 self.session.flash(
410 411 _('Given reset token is invalid'), queue='error')
411 412 return HTTPFound(self.request.route_path('reset_password'))
412 413
413 414 try:
414 415 owner = token.user
415 416 data = {'email': owner.email, 'token': token.api_key}
416 417 UserModel().reset_password(data)
417 418 self.session.flash(
418 419 _('Your password reset was successful, '
419 420 'a new password has been sent to your email'),
420 421 queue='success')
421 422 except Exception as e:
422 423 log.error(e)
423 424 return HTTPFound(self.request.route_path('reset_password'))
424 425
425 426 return HTTPFound(self.request.route_path('login'))
@@ -1,98 +1,98 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 import deform
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.forms import RcForm
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator,
33 33 HasRepoPermissionAllDecorator, CSRFRequired)
34 34 from rhodecode.model.db import RepositoryField, RepoGroup
35 35 from rhodecode.model.forms import RepoPermsForm
36 36 from rhodecode.model.meta import Session
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.scm import RepoGroupList, ScmModel
39 39 from rhodecode.model.validation_schema.schemas import repo_schema
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class RepoSettingsPermissionsView(RepoAppView):
45 45
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context()
48 48
49 49 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
50 50 c.repo_info = self.db_repo
51 51
52 52 self._register_global_c(c)
53 53 return c
54 54
55 55 @LoginRequired()
56 56 @HasRepoPermissionAnyDecorator('repository.admin')
57 57 @view_config(
58 58 route_name='edit_repo_perms', request_method='GET',
59 59 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
60 60 def edit_permissions(self):
61 61 c = self.load_default_context()
62 62 c.active = 'permissions'
63 63 return self._get_template_context(c)
64 64
65 65 @LoginRequired()
66 66 @HasRepoPermissionAllDecorator('repository.admin')
67 67 @CSRFRequired()
68 68 @view_config(
69 69 route_name='edit_repo_perms', request_method='POST',
70 70 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
71 71 def edit_permissions_update(self):
72 72 _ = self.request.translate
73 73 c = self.load_default_context()
74 74 c.active = 'permissions'
75 75 data = self.request.POST
76 76 # store private flag outside of HTML to verify if we can modify
77 77 # default user permissions, prevents submition of FAKE post data
78 78 # into the form for private repos
79 79 data['repo_private'] = self.db_repo.private
80 80 form = RepoPermsForm()().to_python(data)
81 81 changes = RepoModel().update_permissions(
82 82 self.db_repo_name, form['perm_additions'], form['perm_updates'],
83 83 form['perm_deletions'])
84 84
85 85 action_data = {
86 86 'added': changes['added'],
87 87 'updated': changes['updated'],
88 88 'deleted': changes['deleted'],
89 89 }
90 audit_logger.store(
90 audit_logger.store_web(
91 91 'repo.edit.permissions', action_data=action_data,
92 92 user=self._rhodecode_user, repo=self.db_repo)
93 93
94 94 Session().commit()
95 95 h.flash(_('Repository permissions updated'), category='success')
96 96
97 97 raise HTTPFound(
98 98 self.request.route_path('edit_repo_perms', repo_name=self.db_repo_name))
@@ -1,179 +1,179 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 import deform
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.forms import RcForm
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator,
33 33 HasRepoPermissionAllDecorator, CSRFRequired)
34 34 from rhodecode.model.db import RepositoryField, RepoGroup
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.model.repo import RepoModel
37 37 from rhodecode.model.scm import RepoGroupList, ScmModel
38 38 from rhodecode.model.validation_schema.schemas import repo_schema
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class RepoSettingsView(RepoAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47
48 48 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
49 49 c.repo_info = self.db_repo
50 50
51 51 acl_groups = RepoGroupList(
52 52 RepoGroup.query().all(),
53 53 perm_set=['group.write', 'group.admin'])
54 54 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
55 55 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
56 56
57 57 # in case someone no longer have a group.write access to a repository
58 58 # pre fill the list with this entry, we don't care if this is the same
59 59 # but it will allow saving repo data properly.
60 60 repo_group = self.db_repo.group
61 61 if repo_group and repo_group.group_id not in c.repo_groups_choices:
62 62 c.repo_groups_choices.append(repo_group.group_id)
63 63 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
64 64
65 65 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
66 66 # we might be in missing requirement state, so we load things
67 67 # without touching scm_instance()
68 68 c.landing_revs_choices, c.landing_revs = \
69 69 ScmModel().get_repo_landing_revs()
70 70 else:
71 71 c.landing_revs_choices, c.landing_revs = \
72 72 ScmModel().get_repo_landing_revs(self.db_repo)
73 73
74 74 c.personal_repo_group = c.auth_user.personal_repo_group
75 75 c.repo_fields = RepositoryField.query()\
76 76 .filter(RepositoryField.repository == self.db_repo).all()
77 77
78 78 self._register_global_c(c)
79 79 return c
80 80
81 81 def _get_schema(self, c, old_values=None):
82 82 return repo_schema.RepoSettingsSchema().bind(
83 83 repo_type=self.db_repo.repo_type,
84 84 repo_type_options=[self.db_repo.repo_type],
85 85 repo_ref_options=c.landing_revs_choices,
86 86 repo_ref_items=c.landing_revs,
87 87 repo_repo_group_options=c.repo_groups_choices,
88 88 repo_repo_group_items=c.repo_groups,
89 89 # user caller
90 90 user=self._rhodecode_user,
91 91 old_values=old_values
92 92 )
93 93
94 94 @LoginRequired()
95 95 @HasRepoPermissionAnyDecorator('repository.admin')
96 96 @view_config(
97 97 route_name='edit_repo', request_method='GET',
98 98 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
99 99 def edit_settings(self):
100 100 c = self.load_default_context()
101 101 c.active = 'settings'
102 102
103 103 defaults = RepoModel()._get_defaults(self.db_repo_name)
104 104 defaults['repo_owner'] = defaults['user']
105 105 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
106 106
107 107 schema = self._get_schema(c)
108 108 c.form = RcForm(schema, appstruct=defaults)
109 109 return self._get_template_context(c)
110 110
111 111 @LoginRequired()
112 112 @HasRepoPermissionAllDecorator('repository.admin')
113 113 @CSRFRequired()
114 114 @view_config(
115 115 route_name='edit_repo', request_method='POST',
116 116 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
117 117 def edit_settings_update(self):
118 118 _ = self.request.translate
119 119 c = self.load_default_context()
120 120 c.active = 'settings'
121 121 old_repo_name = self.db_repo_name
122 122
123 123 old_values = self.db_repo.get_api_data()
124 124 schema = self._get_schema(c, old_values=old_values)
125 125
126 126 c.form = RcForm(schema)
127 127 pstruct = self.request.POST.items()
128 128 pstruct.append(('repo_type', self.db_repo.repo_type))
129 129 try:
130 130 schema_data = c.form.validate(pstruct)
131 131 except deform.ValidationFailure as err_form:
132 132 return self._get_template_context(c)
133 133
134 134 # data is now VALID, proceed with updates
135 135 # save validated data back into the updates dict
136 136 validated_updates = dict(
137 137 repo_name=schema_data['repo_group']['repo_name_without_group'],
138 138 repo_group=schema_data['repo_group']['repo_group_id'],
139 139
140 140 user=schema_data['repo_owner'],
141 141 repo_description=schema_data['repo_description'],
142 142 repo_private=schema_data['repo_private'],
143 143 clone_uri=schema_data['repo_clone_uri'],
144 144 repo_landing_rev=schema_data['repo_landing_commit_ref'],
145 145 repo_enable_statistics=schema_data['repo_enable_statistics'],
146 146 repo_enable_locking=schema_data['repo_enable_locking'],
147 147 repo_enable_downloads=schema_data['repo_enable_downloads'],
148 148 )
149 149 # detect if CLONE URI changed, if we get OLD means we keep old values
150 150 if schema_data['repo_clone_uri_change'] == 'OLD':
151 151 validated_updates['clone_uri'] = self.db_repo.clone_uri
152 152
153 153 # use the new full name for redirect
154 154 new_repo_name = schema_data['repo_group']['repo_name_with_group']
155 155
156 156 # save extra fields into our validated data
157 157 for key, value in pstruct:
158 158 if key.startswith(RepositoryField.PREFIX):
159 159 validated_updates[key] = value
160 160
161 161 try:
162 162 RepoModel().update(self.db_repo, **validated_updates)
163 163 ScmModel().mark_for_invalidation(new_repo_name)
164 164
165 audit_logger.store(
165 audit_logger.store_web(
166 166 'repo.edit', action_data={'old_data': old_values},
167 167 user=self._rhodecode_user, repo=self.db_repo)
168 168
169 169 Session().commit()
170 170
171 171 h.flash(_('Repository {} updated successfully').format(
172 172 old_repo_name), category='success')
173 173 except Exception:
174 174 log.exception("Exception during update of repository")
175 175 h.flash(_('Error occurred during update of repository {}').format(
176 176 old_repo_name), category='error')
177 177
178 178 raise HTTPFound(
179 179 self.request.route_path('edit_repo', repo_name=new_repo_name))
@@ -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 @view_config(
77 77 route_name='edit_repo_advanced_delete', request_method='POST',
78 78 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
79 79 def edit_advanced_delete(self):
80 80 """
81 81 Deletes the repository, or shows warnings if deletion is not possible
82 82 because of attached forks or other errors.
83 83 """
84 84 _ = self.request.translate
85 85 handle_forks = self.request.POST.get('forks', None)
86 86
87 87 try:
88 88 _forks = self.db_repo.forks.count()
89 89 if _forks and handle_forks:
90 90 if handle_forks == 'detach_forks':
91 91 handle_forks = 'detach'
92 92 h.flash(_('Detached %s forks') % _forks, category='success')
93 93 elif handle_forks == 'delete_forks':
94 94 handle_forks = 'delete'
95 95 h.flash(_('Deleted %s forks') % _forks, category='success')
96 96
97 97 repo_data = self.db_repo.get_api_data()
98 98 RepoModel().delete(self.db_repo, forks=handle_forks)
99 99
100 100 repo = audit_logger.RepoWrap(repo_id=None,
101 101 repo_name=self.db_repo.repo_name)
102 audit_logger.store(
102 audit_logger.store_web(
103 103 action='repo.delete',
104 action_data={'repo_data': repo_data,
105 'source': audit_logger.SOURCE_WEB},
106 user=self._rhodecode_user, repo=repo, commit=False)
104 action_data={'data': repo_data},
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,114 +1,114 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.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
27 27 NotAnonymous)
28 28 from rhodecode.lib.ext_json import json
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class StripView(RepoAppView):
34 34 def load_default_context(self):
35 35 c = self._get_local_tmpl_context()
36 36
37 37 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
38 38 c.repo_info = self.db_repo
39 39
40 40 self._register_global_c(c)
41 41 return c
42 42
43 43 @LoginRequired()
44 44 @HasRepoPermissionAnyDecorator('repository.admin')
45 45 @view_config(
46 46 route_name='strip', request_method='GET',
47 47 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
48 48 def strip(self):
49 49 c = self.load_default_context()
50 50 c.active = 'strip'
51 51 c.strip_limit = 10
52 52
53 53 return self._get_template_context(c)
54 54
55 55 @LoginRequired()
56 56 @HasRepoPermissionAnyDecorator('repository.admin')
57 57 @view_config(
58 58 route_name='strip_check', request_method='POST',
59 59 renderer='json', xhr=True)
60 60 def strip_check(self):
61 61 from rhodecode.lib.vcs.backends.base import EmptyCommit
62 62 data = {}
63 63 rp = self.request.POST
64 64 for i in range(1, 11):
65 65 chset = 'changeset_id-%d' % (i,)
66 66 check = rp.get(chset)
67 67 if check:
68 68 data[i] = self.db_repo.get_changeset(rp[chset])
69 69 if isinstance(data[i], EmptyCommit):
70 70 data[i] = {'rev': None, 'commit': rp[chset]}
71 71 else:
72 72 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
73 73 'author': data[i].author,
74 74 'comment': data[i].message}
75 75 else:
76 76 break
77 77 return data
78 78
79 79 @LoginRequired()
80 80 @HasRepoPermissionAnyDecorator('repository.admin')
81 81 @view_config(
82 82 route_name='strip_execute', request_method='POST',
83 83 renderer='json', xhr=True)
84 84 def strip_execute(self):
85 85 from rhodecode.model.scm import ScmModel
86 86
87 87 c = self.load_default_context()
88 88 user = self._rhodecode_user
89 89 rp = self.request.POST
90 90 data = {}
91 91 for idx in rp:
92 92 commit = json.loads(rp[idx])
93 93 # If someone put two times the same branch
94 94 if commit['branch'] in data.keys():
95 95 continue
96 96 try:
97 97 ScmModel().strip(
98 98 repo=c.repo_info,
99 99 commit_id=commit['rev'], branch=commit['branch'])
100 100 log.info('Stripped commit %s from repo `%s` by %s' % (
101 101 commit['rev'], c.repo_info.repo_name, user))
102 102 data[commit['rev']] = True
103 103
104 audit_logger.store(
104 audit_logger.store_web(
105 105 action='repo.commit.strip',
106 106 action_data={'commit_id': commit['rev']},
107 107 repo=self.db_repo,
108 108 user=self._rhodecode_user, commit=True)
109 109
110 110 except Exception as e:
111 111 data[commit['rev']] = False
112 112 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (
113 113 commit['rev'], self.db_repo_name, user, e.message))
114 114 return data
@@ -1,406 +1,404 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 188 Session().commit()
189 189 repo_group_data = repo_group.get_api_data()
190 190 _new_group_name = form_result['group_name_full']
191 191
192 audit_logger.store(
192 audit_logger.store_web(
193 193 action='repo_group.create',
194 action_data={'repo_group_data': repo_group_data},
194 action_data={'data': repo_group_data},
195 195 user=c.rhodecode_user, commit=True)
196 196
197 197 repo_group_url = h.link_to(
198 198 _new_group_name,
199 199 h.route_path('repo_group_home', repo_group_name=_new_group_name))
200 200 h.flash(h.literal(_('Created repository group %s')
201 201 % repo_group_url), category='success')
202 202
203 203 except formencode.Invalid as errors:
204 204 return htmlfill.render(
205 205 render('admin/repo_groups/repo_group_add.mako'),
206 206 defaults=errors.value,
207 207 errors=errors.error_dict or {},
208 208 prefix_error=False,
209 209 encoding="UTF-8",
210 210 force_defaults=False)
211 211 except Exception:
212 212 log.exception("Exception during creation of repository group")
213 213 h.flash(_('Error occurred during creation of repository group %s')
214 214 % request.POST.get('group_name'), category='error')
215 215
216 216 # TODO: maybe we should get back to the main view, not the admin one
217 217 return redirect(url('repo_groups', parent_group=parent_group_id))
218 218
219 219 # perm checks inside
220 220 @NotAnonymous()
221 221 def new(self):
222 222 # perm check for admin, create_group perm or admin of parent_group
223 223 parent_group_id = safe_int(request.GET.get('parent_group'))
224 224 if not self._can_create_repo_group(parent_group_id):
225 225 return abort(403)
226 226
227 227 self.__load_defaults()
228 228 return render('admin/repo_groups/repo_group_add.mako')
229 229
230 230 @HasRepoGroupPermissionAnyDecorator('group.admin')
231 231 @auth.CSRFRequired()
232 232 def update(self, group_name):
233 233
234 234 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
235 235 can_create_in_root = self._can_create_repo_group()
236 236 show_root_location = can_create_in_root
237 237 if not c.repo_group.parent_group:
238 238 # this group don't have a parrent so we should show empty value
239 239 show_root_location = True
240 240 self.__load_defaults(allow_empty_group=show_root_location,
241 241 repo_group=c.repo_group)
242 242
243 243 repo_group_form = RepoGroupForm(
244 244 edit=True, old_data=c.repo_group.get_dict(),
245 245 available_groups=c.repo_groups_choices,
246 246 can_create_in_root=can_create_in_root, allow_disabled=True)()
247 247
248 248 old_values = c.repo_group.get_api_data()
249 249 try:
250 250 form_result = repo_group_form.to_python(dict(request.POST))
251 251 gr_name = form_result['group_name']
252 252 new_gr = RepoGroupModel().update(group_name, form_result)
253 253
254 audit_logger.store(
254 audit_logger.store_web(
255 255 'repo_group.edit', action_data={'old_data': old_values},
256 256 user=c.rhodecode_user)
257 257
258 258 Session().commit()
259 259 h.flash(_('Updated repository group %s') % (gr_name,),
260 260 category='success')
261 261 # we now have new name !
262 262 group_name = new_gr.group_name
263 263 except formencode.Invalid as errors:
264 264 c.active = 'settings'
265 265 return htmlfill.render(
266 266 render('admin/repo_groups/repo_group_edit.mako'),
267 267 defaults=errors.value,
268 268 errors=errors.error_dict or {},
269 269 prefix_error=False,
270 270 encoding="UTF-8",
271 271 force_defaults=False)
272 272 except Exception:
273 273 log.exception("Exception during update or repository group")
274 274 h.flash(_('Error occurred during update of repository group %s')
275 275 % request.POST.get('group_name'), category='error')
276 276
277 277 return redirect(url('edit_repo_group', group_name=group_name))
278 278
279 279 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 280 @auth.CSRFRequired()
281 281 def delete(self, group_name):
282 282 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
283 283 repos = gr.repositories.all()
284 284 if repos:
285 285 msg = ungettext(
286 286 'This group contains %(num)d repository and cannot be deleted',
287 287 'This group contains %(num)d repositories and cannot be'
288 288 ' deleted',
289 289 len(repos)) % {'num': len(repos)}
290 290 h.flash(msg, category='warning')
291 291 return redirect(url('repo_groups'))
292 292
293 293 children = gr.children.all()
294 294 if children:
295 295 msg = ungettext(
296 296 'This group contains %(num)d subgroup and cannot be deleted',
297 297 'This group contains %(num)d subgroups and cannot be deleted',
298 298 len(children)) % {'num': len(children)}
299 299 h.flash(msg, category='warning')
300 300 return redirect(url('repo_groups'))
301 301
302 302 try:
303 303 old_values = gr.get_api_data()
304 304 RepoGroupModel().delete(group_name)
305 305
306 audit_logger.store(
306 audit_logger.store_web(
307 307 'repo_group.delete',
308 action_data={'old_data': old_values,
309 'source': audit_logger.SOURCE_WEB},
308 action_data={'old_data': old_values},
310 309 user=c.rhodecode_user)
311 310
312 311 Session().commit()
313 312 h.flash(_('Removed repository group %s') % group_name,
314 313 category='success')
315 314 except Exception:
316 315 log.exception("Exception during deletion of repository group")
317 316 h.flash(_('Error occurred during deletion of repository group %s')
318 317 % group_name, category='error')
319 318
320 319 return redirect(url('repo_groups'))
321 320
322 321 @HasRepoGroupPermissionAnyDecorator('group.admin')
323 322 def edit(self, group_name):
324 323
325 324 c.active = 'settings'
326 325
327 326 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
328 327 # we can only allow moving empty group if it's already a top-level
329 328 # group, ie has no parents, or we're admin
330 329 can_create_in_root = self._can_create_repo_group()
331 330 show_root_location = can_create_in_root
332 331 if not c.repo_group.parent_group:
333 332 # this group don't have a parrent so we should show empty value
334 333 show_root_location = True
335 334 self.__load_defaults(allow_empty_group=show_root_location,
336 335 repo_group=c.repo_group)
337 336 defaults = self.__load_data(c.repo_group.group_id)
338 337
339 338 return htmlfill.render(
340 339 render('admin/repo_groups/repo_group_edit.mako'),
341 340 defaults=defaults,
342 341 encoding="UTF-8",
343 342 force_defaults=False
344 343 )
345 344
346 345 @HasRepoGroupPermissionAnyDecorator('group.admin')
347 346 def edit_repo_group_advanced(self, group_name):
348 347 c.active = 'advanced'
349 348 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
350 349
351 350 return render('admin/repo_groups/repo_group_edit.mako')
352 351
353 352 @HasRepoGroupPermissionAnyDecorator('group.admin')
354 353 def edit_repo_group_perms(self, group_name):
355 354 c.active = 'perms'
356 355 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
357 356 self.__load_defaults()
358 357 defaults = self.__load_data(c.repo_group.group_id)
359 358
360 359 return htmlfill.render(
361 360 render('admin/repo_groups/repo_group_edit.mako'),
362 361 defaults=defaults,
363 362 encoding="UTF-8",
364 363 force_defaults=False
365 364 )
366 365
367 366 @HasRepoGroupPermissionAnyDecorator('group.admin')
368 367 @auth.CSRFRequired()
369 368 def update_perms(self, group_name):
370 369 """
371 370 Update permissions for given repository group
372 371 """
373 372
374 373 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
375 374 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
376 375 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
377 376 request.POST)
378 377
379 378 if not c.rhodecode_user.is_admin:
380 379 if self._revoke_perms_on_yourself(form):
381 380 msg = _('Cannot change permission for yourself as admin')
382 381 h.flash(msg, category='warning')
383 382 return redirect(
384 383 url('edit_repo_group_perms', group_name=group_name))
385 384
386 385 # iterate over all members(if in recursive mode) of this groups and
387 386 # set the permissions !
388 387 # this can be potentially heavy operation
389 388 changes = RepoGroupModel().update_permissions(
390 389 c.repo_group,
391 390 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
392 391 form['recursive'])
393 392
394 393 action_data = {
395 394 'added': changes['added'],
396 395 'updated': changes['updated'],
397 396 'deleted': changes['deleted'],
398 'source': audit_logger.SOURCE_WEB
399 397 }
400 audit_logger.store(
398 audit_logger.store_web(
401 399 'repo_group.edit.permissions', action_data=action_data,
402 400 user=c.rhodecode_user)
403 401
404 402 Session().commit()
405 403 h.flash(_('Repository Group permissions updated'), category='success')
406 404 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -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, action_logger
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 msg = _('No such commit exists for this repository')
128 128 log.exception(msg)
129 129 h.flash(msg, category='error')
130 130 raise HTTPNotFound()
131 131 except RepositoryError as e:
132 132 h.flash(safe_str(e), category='error')
133 133 raise HTTPNotFound()
134 134
135 135 return file_node
136 136
137 137 def __get_tree_cache_manager(self, repo_name, namespace_type):
138 138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
139 139 return caches.get_cache_manager('repo_cache_long', _namespace)
140 140
141 141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
142 142 full_load=False, force=False):
143 143 def _cached_tree():
144 144 log.debug('Generating cached file tree for %s, %s, %s',
145 145 repo_name, commit_id, f_path)
146 146 c.full_load = full_load
147 147 return render('files/files_browser_tree.mako')
148 148
149 149 cache_manager = self.__get_tree_cache_manager(
150 150 repo_name, caches.FILE_TREE)
151 151
152 152 cache_key = caches.compute_key_from_params(
153 153 repo_name, commit_id, f_path)
154 154
155 155 if force:
156 156 # we want to force recompute of caches
157 157 cache_manager.remove_value(cache_key)
158 158
159 159 return cache_manager.get(cache_key, createfunc=_cached_tree)
160 160
161 161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
162 162 def _cached_nodes():
163 163 log.debug('Generating cached nodelist for %s, %s, %s',
164 164 repo_name, commit_id, f_path)
165 165 _d, _f = ScmModel().get_nodes(
166 166 repo_name, commit_id, f_path, flat=False)
167 167 return _d + _f
168 168
169 169 cache_manager = self.__get_tree_cache_manager(
170 170 repo_name, caches.FILE_SEARCH_TREE_META)
171 171
172 172 cache_key = caches.compute_key_from_params(
173 173 repo_name, commit_id, f_path)
174 174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
175 175
176 176 @LoginRequired()
177 177 @HasRepoPermissionAnyDecorator(
178 178 'repository.read', 'repository.write', 'repository.admin')
179 179 def index(
180 180 self, repo_name, revision, f_path, annotate=False, rendered=False):
181 181 commit_id = revision
182 182
183 183 # redirect to given commit_id from form if given
184 184 get_commit_id = request.GET.get('at_rev', None)
185 185 if get_commit_id:
186 186 self.__get_commit_or_redirect(get_commit_id, repo_name)
187 187
188 188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
189 189 c.branch = request.GET.get('branch', None)
190 190 c.f_path = f_path
191 191 c.annotate = annotate
192 192 # default is false, but .rst/.md files later are autorendered, we can
193 193 # overwrite autorendering by setting this GET flag
194 194 c.renderer = rendered or not request.GET.get('no-render', False)
195 195
196 196 # prev link
197 197 try:
198 198 prev_commit = c.commit.prev(c.branch)
199 199 c.prev_commit = prev_commit
200 200 c.url_prev = url('files_home', repo_name=c.repo_name,
201 201 revision=prev_commit.raw_id, f_path=f_path)
202 202 if c.branch:
203 203 c.url_prev += '?branch=%s' % c.branch
204 204 except (CommitDoesNotExistError, VCSError):
205 205 c.url_prev = '#'
206 206 c.prev_commit = EmptyCommit()
207 207
208 208 # next link
209 209 try:
210 210 next_commit = c.commit.next(c.branch)
211 211 c.next_commit = next_commit
212 212 c.url_next = url('files_home', repo_name=c.repo_name,
213 213 revision=next_commit.raw_id, f_path=f_path)
214 214 if c.branch:
215 215 c.url_next += '?branch=%s' % c.branch
216 216 except (CommitDoesNotExistError, VCSError):
217 217 c.url_next = '#'
218 218 c.next_commit = EmptyCommit()
219 219
220 220 # files or dirs
221 221 try:
222 222 c.file = c.commit.get_node(f_path)
223 223 c.file_author = True
224 224 c.file_tree = ''
225 225 if c.file.is_file():
226 226 c.lf_node = c.file.get_largefile_node()
227 227
228 228 c.file_source_page = 'true'
229 229 c.file_last_commit = c.file.last_commit
230 230 if c.file.size < self.cut_off_limit_file:
231 231 if c.annotate: # annotation has precedence over renderer
232 232 c.annotated_lines = filenode_as_annotated_lines_tokens(
233 233 c.file
234 234 )
235 235 else:
236 236 c.renderer = (
237 237 c.renderer and h.renderer_from_filename(c.file.path)
238 238 )
239 239 if not c.renderer:
240 240 c.lines = filenode_as_lines_tokens(c.file)
241 241
242 242 c.on_branch_head = self._is_valid_head(
243 243 commit_id, c.rhodecode_repo)
244 244
245 245 branch = c.commit.branch if (
246 246 c.commit.branch and '/' not in c.commit.branch) else None
247 247 c.branch_or_raw_id = branch or c.commit.raw_id
248 248 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
249 249
250 250 author = c.file_last_commit.author
251 251 c.authors = [(h.email(author),
252 252 h.person(author, 'username_or_name_or_email'))]
253 253 else:
254 254 c.file_source_page = 'false'
255 255 c.authors = []
256 256 c.file_tree = self._get_tree_at_commit(
257 257 repo_name, c.commit.raw_id, f_path)
258 258
259 259 except RepositoryError as e:
260 260 h.flash(safe_str(e), category='error')
261 261 raise HTTPNotFound()
262 262
263 263 if request.environ.get('HTTP_X_PJAX'):
264 264 return render('files/files_pjax.mako')
265 265
266 266 return render('files/files.mako')
267 267
268 268 @LoginRequired()
269 269 @HasRepoPermissionAnyDecorator(
270 270 'repository.read', 'repository.write', 'repository.admin')
271 271 def annotate_previous(self, repo_name, revision, f_path):
272 272
273 273 commit_id = revision
274 274 commit = self.__get_commit_or_redirect(commit_id, repo_name)
275 275 prev_commit_id = commit.raw_id
276 276
277 277 f_path = f_path
278 278 is_file = False
279 279 try:
280 280 _file = commit.get_node(f_path)
281 281 is_file = _file.is_file()
282 282 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
283 283 pass
284 284
285 285 if is_file:
286 286 history = commit.get_file_history(f_path)
287 287 prev_commit_id = history[1].raw_id \
288 288 if len(history) > 1 else prev_commit_id
289 289
290 290 return redirect(h.url(
291 291 'files_annotate_home', repo_name=repo_name,
292 292 revision=prev_commit_id, f_path=f_path))
293 293
294 294 @LoginRequired()
295 295 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 296 'repository.admin')
297 297 @jsonify
298 298 def history(self, repo_name, revision, f_path):
299 299 commit = self.__get_commit_or_redirect(revision, repo_name)
300 300 f_path = f_path
301 301 _file = commit.get_node(f_path)
302 302 if _file.is_file():
303 303 file_history, _hist = self._get_node_history(commit, f_path)
304 304
305 305 res = []
306 306 for obj in file_history:
307 307 res.append({
308 308 'text': obj[1],
309 309 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
310 310 })
311 311
312 312 data = {
313 313 'more': False,
314 314 'results': res
315 315 }
316 316 return data
317 317
318 318 @LoginRequired()
319 319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 320 'repository.admin')
321 321 def authors(self, repo_name, revision, f_path):
322 322 commit = self.__get_commit_or_redirect(revision, repo_name)
323 323 file_node = commit.get_node(f_path)
324 324 if file_node.is_file():
325 325 c.file_last_commit = file_node.last_commit
326 326 if request.GET.get('annotate') == '1':
327 327 # use _hist from annotation if annotation mode is on
328 328 commit_ids = set(x[1] for x in file_node.annotate)
329 329 _hist = (
330 330 c.rhodecode_repo.get_commit(commit_id)
331 331 for commit_id in commit_ids)
332 332 else:
333 333 _f_history, _hist = self._get_node_history(commit, f_path)
334 334 c.file_author = False
335 335 c.authors = []
336 336 for author in set(commit.author for commit in _hist):
337 337 c.authors.append((
338 338 h.email(author),
339 339 h.person(author, 'username_or_name_or_email')))
340 340 return render('files/file_authors_box.mako')
341 341
342 342 @LoginRequired()
343 343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
344 344 'repository.admin')
345 345 def rawfile(self, repo_name, revision, f_path):
346 346 """
347 347 Action for download as raw
348 348 """
349 349 commit = self.__get_commit_or_redirect(revision, repo_name)
350 350 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
351 351
352 352 if request.GET.get('lf'):
353 353 # only if lf get flag is passed, we download this file
354 354 # as LFS/Largefile
355 355 lf_node = file_node.get_largefile_node()
356 356 if lf_node:
357 357 # overwrite our pointer with the REAL large-file
358 358 file_node = lf_node
359 359
360 360 response.content_disposition = 'attachment; filename=%s' % \
361 361 safe_str(f_path.split(Repository.NAME_SEP)[-1])
362 362
363 363 response.content_type = file_node.mimetype
364 364 charset = self._get_default_encoding()
365 365 if charset:
366 366 response.charset = charset
367 367
368 368 return file_node.content
369 369
370 370 @LoginRequired()
371 371 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
372 372 'repository.admin')
373 373 def raw(self, repo_name, revision, f_path):
374 374 """
375 375 Action for show as raw, some mimetypes are "rendered",
376 376 those include images, icons.
377 377 """
378 378 commit = self.__get_commit_or_redirect(revision, repo_name)
379 379 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
380 380
381 381 raw_mimetype_mapping = {
382 382 # map original mimetype to a mimetype used for "show as raw"
383 383 # you can also provide a content-disposition to override the
384 384 # default "attachment" disposition.
385 385 # orig_type: (new_type, new_dispo)
386 386
387 387 # show images inline:
388 388 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
389 389 # for example render an SVG with javascript inside or even render
390 390 # HTML.
391 391 'image/x-icon': ('image/x-icon', 'inline'),
392 392 'image/png': ('image/png', 'inline'),
393 393 'image/gif': ('image/gif', 'inline'),
394 394 'image/jpeg': ('image/jpeg', 'inline'),
395 395 'application/pdf': ('application/pdf', 'inline'),
396 396 }
397 397
398 398 mimetype = file_node.mimetype
399 399 try:
400 400 mimetype, dispo = raw_mimetype_mapping[mimetype]
401 401 except KeyError:
402 402 # we don't know anything special about this, handle it safely
403 403 if file_node.is_binary:
404 404 # do same as download raw for binary files
405 405 mimetype, dispo = 'application/octet-stream', 'attachment'
406 406 else:
407 407 # do not just use the original mimetype, but force text/plain,
408 408 # otherwise it would serve text/html and that might be unsafe.
409 409 # Note: underlying vcs library fakes text/plain mimetype if the
410 410 # mimetype can not be determined and it thinks it is not
411 411 # binary.This might lead to erroneous text display in some
412 412 # cases, but helps in other cases, like with text files
413 413 # without extension.
414 414 mimetype, dispo = 'text/plain', 'inline'
415 415
416 416 if dispo == 'attachment':
417 417 dispo = 'attachment; filename=%s' % safe_str(
418 418 f_path.split(os.sep)[-1])
419 419
420 420 response.content_disposition = dispo
421 421 response.content_type = mimetype
422 422 charset = self._get_default_encoding()
423 423 if charset:
424 424 response.charset = charset
425 425 return file_node.content
426 426
427 427 @CSRFRequired()
428 428 @LoginRequired()
429 429 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
430 430 def delete(self, repo_name, revision, f_path):
431 431 commit_id = revision
432 432
433 433 repo = c.rhodecode_db_repo
434 434 if repo.enable_locking and repo.locked[0]:
435 435 h.flash(_('This repository has been locked by %s on %s')
436 436 % (h.person_by_id(repo.locked[0]),
437 437 h.format_date(h.time_to_datetime(repo.locked[1]))),
438 438 'warning')
439 439 return redirect(h.url('files_home',
440 440 repo_name=repo_name, revision='tip'))
441 441
442 442 if not self._is_valid_head(commit_id, repo.scm_instance()):
443 443 h.flash(_('You can only delete files with revision '
444 444 'being a valid branch '), category='warning')
445 445 return redirect(h.url('files_home',
446 446 repo_name=repo_name, revision='tip',
447 447 f_path=f_path))
448 448
449 449 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
450 450 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
451 451
452 452 c.default_message = _(
453 453 'Deleted file %s via RhodeCode Enterprise') % (f_path)
454 454 c.f_path = f_path
455 455 node_path = f_path
456 456 author = c.rhodecode_user.full_contact
457 457 message = request.POST.get('message') or c.default_message
458 458 try:
459 459 nodes = {
460 460 node_path: {
461 461 'content': ''
462 462 }
463 463 }
464 464 self.scm_model.delete_nodes(
465 465 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
466 466 message=message,
467 467 nodes=nodes,
468 468 parent_commit=c.commit,
469 469 author=author,
470 470 )
471 471
472 472 h.flash(_('Successfully deleted file %s') % f_path,
473 473 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 %s via RhodeCode Enterprise') % (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 %s via RhodeCode Enterprise') % (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(_('Successfully committed to %s') % f_path,
579 579 category='success')
580 580 except Exception:
581 581 msg = _('Error occurred during commit')
582 582 log.exception(msg)
583 583 h.flash(msg, 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 %s via RhodeCode Enterprise') % (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(_('Successfully committed to %s') % node_path,
708 708 category='success')
709 709 except NonRelativePathError as e:
710 710 h.flash(_(
711 711 'The location specified must be a relative path and must not '
712 712 'contain .. in the path'), category='warning')
713 713 return redirect(url('changeset_home', repo_name=c.repo_name,
714 714 revision='tip'))
715 715 except (NodeError, NodeAlreadyExistsError) as e:
716 716 h.flash(_(e), category='error')
717 717 except Exception:
718 718 msg = _('Error occurred during commit')
719 719 log.exception(msg)
720 720 h.flash(msg, 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 audit_logger.store(
817 audit_logger.store_web(
818 818 action='repo.archive.download',
819 819 action_data={'user_agent': request.user_agent,
820 820 'archive_name': archive_name,
821 821 'archive_spec': fname,
822 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,240 +1,240 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 = {
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': {},
44 44 'user.edit.ip.delete': {},
45 45 'user.edit.token.add': {},
46 46 'user.edit.token.delete': {},
47 47 'user.edit.email.add': {},
48 48 'user.edit.email.delete': {},
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 56 'user_group.edit.member.add': {},
57 57 'user_group.edit.member.delete': {},
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': {},
65 65 'repo.archive.download': {},
66 66
67 67 'repo_group.create': {'data': {}},
68 68 'repo_group.edit': {'old_data': {}},
69 69 'repo_group.edit.permissions': {},
70 70 'repo_group.delete': {'old_data': {}},
71 71 }
72 72
73 73 SOURCE_WEB = 'source_web'
74 74 SOURCE_API = 'source_api'
75 75
76 76
77 77 class UserWrap(object):
78 78 """
79 79 Fake object used to imitate AuthUser
80 80 """
81 81
82 82 def __init__(self, user_id=None, username=None, ip_addr=None):
83 83 self.user_id = user_id
84 84 self.username = username
85 85 self.ip_addr = ip_addr
86 86
87 87
88 88 class RepoWrap(object):
89 89 """
90 90 Fake object used to imitate RepoObject that audit logger requires
91 91 """
92 92
93 93 def __init__(self, repo_id=None, repo_name=None):
94 94 self.repo_id = repo_id
95 95 self.repo_name = repo_name
96 96
97 97
98 98 def _store_log(action_name, action_data, user_id, username, user_data,
99 99 ip_address, repository_id, repository_name):
100 100 user_log = UserLog()
101 101 user_log.version = UserLog.VERSION_2
102 102
103 103 user_log.action = action_name
104 104 user_log.action_data = action_data
105 105
106 106 user_log.user_ip = ip_address
107 107
108 108 user_log.user_id = user_id
109 109 user_log.username = username
110 110 user_log.user_data = user_data
111 111
112 112 user_log.repository_id = repository_id
113 113 user_log.repository_name = repository_name
114 114
115 115 user_log.action_date = datetime.datetime.now()
116 116
117 117 log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s',
118 118 action_name, user_id, username, ip_address)
119 119
120 120 return user_log
121 121
122 122
123 123 def store_web(*args, **kwargs):
124 124 if 'action_data' not in kwargs:
125 125 kwargs['action_data'] = {}
126 126 kwargs['action_data'].update({
127 127 'source': SOURCE_WEB
128 128 })
129 129 return store(*args, **kwargs)
130 130
131 131
132 132 def store_api(*args, **kwargs):
133 133 if 'action_data' not in kwargs:
134 134 kwargs['action_data'] = {}
135 135 kwargs['action_data'].update({
136 136 'source': SOURCE_API
137 137 })
138 138 return store(*args, **kwargs)
139 139
140 140
141 141 def store(action, user, action_data=None, user_data=None, ip_addr=None,
142 142 repo=None, sa_session=None, commit=False):
143 143 """
144 144 Audit logger for various actions made by users, typically this
145 145 results in a call such::
146 146
147 147 from rhodecode.lib import audit_logger
148 148
149 149 audit_logger.store(
150 150 action='repo.edit', user=self._rhodecode_user)
151 151 audit_logger.store(
152 action='repo.delete', action_data={'repo_data': repo_data},
152 action='repo.delete', action_data={'data': repo_data},
153 153 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
154 154
155 155 # repo action
156 156 audit_logger.store(
157 157 action='repo.delete',
158 158 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
159 159 repo=audit_logger.RepoWrap(repo_name='some-repo'))
160 160
161 161 # repo action, when we know and have the repository object already
162 162 audit_logger.store(
163 163 action='repo.delete',
164 164 action_data={'source': audit_logger.SOURCE_WEB, },
165 165 user=self._rhodecode_user,
166 166 repo=repo_object)
167 167
168 168 # alternative wrapper to the above
169 169 audit_logger.store_web(
170 170 action='repo.delete',
171 171 action_data={},
172 172 user=self._rhodecode_user,
173 173 repo=repo_object)
174 174
175 175 # without an user ?
176 176 audit_logger.store(
177 177 action='user.login.failure',
178 178 user=audit_logger.UserWrap(
179 179 username=self.request.params.get('username'),
180 180 ip_addr=self.request.remote_addr))
181 181
182 182 """
183 183 from rhodecode.lib.utils2 import safe_unicode
184 184 from rhodecode.lib.auth import AuthUser
185 185
186 186 action_spec = ACTIONS.get(action, None)
187 187 if action_spec is None:
188 188 raise ValueError('Action `{}` is not supported'.format(action))
189 189
190 190 if not sa_session:
191 191 sa_session = meta.Session()
192 192
193 193 try:
194 194 username = getattr(user, 'username', None)
195 195 if not username:
196 196 pass
197 197
198 198 user_id = getattr(user, 'user_id', None)
199 199 if not user_id:
200 200 # maybe we have username ? Try to figure user_id from username
201 201 if username:
202 202 user_id = getattr(
203 203 User.get_by_username(username), 'user_id', None)
204 204
205 205 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
206 206 if not ip_addr:
207 207 pass
208 208
209 209 if not user_data:
210 210 # try to get this from the auth user
211 211 if isinstance(user, AuthUser):
212 212 user_data = {
213 213 'username': user.username,
214 214 'email': user.email,
215 215 }
216 216
217 217 repository_name = getattr(repo, 'repo_name', None)
218 218 repository_id = getattr(repo, 'repo_id', None)
219 219 if not repository_id:
220 220 # maybe we have repo_name ? Try to figure repo_id from repo_name
221 221 if repository_name:
222 222 repository_id = getattr(
223 223 Repository.get_by_repo_name(repository_name), 'repo_id', None)
224 224
225 225 user_log = _store_log(
226 226 action_name=safe_unicode(action),
227 227 action_data=action_data or {},
228 228 user_id=user_id,
229 229 username=username,
230 230 user_data=user_data or {},
231 231 ip_address=safe_unicode(ip_addr),
232 232 repository_id=repository_id,
233 233 repository_name=repository_name
234 234 )
235 235 sa_session.add(user_log)
236 236 if commit:
237 237 sa_session.commit()
238 238
239 239 except Exception:
240 240 log.exception('AUDIT: failed to store audit log')
@@ -1,299 +1,299 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(
182 audit_logger.store_web(
183 183 action='repo.create',
184 184 action_data={'data': repo_data},
185 185 user=cur_user,
186 186 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
187 187
188 188 DBS.commit()
189 189 except Exception:
190 190 log.warning('Exception occurred when creating repository, '
191 191 'doing cleanup...', exc_info=True)
192 192 # rollback things manually !
193 193 repo = Repository.get_by_repo_name(repo_name_full)
194 194 if repo:
195 195 Repository.delete(repo.repo_id)
196 196 DBS.commit()
197 197 RepoModel(DBS)._delete_filesystem_repo(repo)
198 198 raise
199 199
200 200 # it's an odd fix to make celery fail task when exception occurs
201 201 def on_failure(self, *args, **kwargs):
202 202 pass
203 203
204 204 return True
205 205
206 206
207 207 @task(ignore_result=True, base=RhodecodeCeleryTask)
208 208 @dbsession
209 209 @vcsconnection
210 210 def create_repo_fork(form_data, cur_user):
211 211 """
212 212 Creates a fork of repository using internal VCS methods
213 213
214 214 :param form_data:
215 215 :param cur_user:
216 216 """
217 217 from rhodecode.model.repo import RepoModel
218 218 from rhodecode.model.user import UserModel
219 219
220 220 log = get_logger(create_repo_fork)
221 221 DBS = get_session()
222 222
223 223 cur_user = UserModel(DBS)._get_user(cur_user)
224 224 owner = cur_user
225 225
226 226 repo_name = form_data['repo_name'] # fork in this case
227 227 repo_name_full = form_data['repo_name_full']
228 228 repo_type = form_data['repo_type']
229 229 description = form_data['description']
230 230 private = form_data['private']
231 231 clone_uri = form_data.get('clone_uri')
232 232 repo_group = safe_int(form_data['repo_group'])
233 233 landing_rev = form_data['landing_rev']
234 234 copy_fork_permissions = form_data.get('copy_permissions')
235 235 fork_id = safe_int(form_data.get('fork_parent_id'))
236 236
237 237 try:
238 238 fork_of = RepoModel(DBS)._get_repo(fork_id)
239 239 RepoModel(DBS)._create_repo(
240 240 repo_name=repo_name_full,
241 241 repo_type=repo_type,
242 242 description=description,
243 243 owner=owner,
244 244 private=private,
245 245 clone_uri=clone_uri,
246 246 repo_group=repo_group,
247 247 landing_rev=landing_rev,
248 248 fork_of=fork_of,
249 249 copy_fork_permissions=copy_fork_permissions
250 250 )
251 251
252 252 DBS.commit()
253 253
254 254 base_path = Repository.base_path()
255 255 source_repo_path = os.path.join(base_path, fork_of.repo_name)
256 256
257 257 # now create this repo on Filesystem
258 258 RepoModel(DBS)._create_filesystem_repo(
259 259 repo_name=repo_name,
260 260 repo_type=repo_type,
261 261 repo_group=RepoModel(DBS)._get_repo_group(repo_group),
262 262 clone_uri=source_repo_path,
263 263 )
264 264 repo = Repository.get_by_repo_name(repo_name_full)
265 265 log_create_repository(created_by=owner.username, **repo.get_dict())
266 266
267 267 # update repo commit caches initially
268 268 config = repo._config
269 269 config.set('extensions', 'largefiles', '')
270 270 repo.update_commit_cache(config=config)
271 271
272 272 # set new created state
273 273 repo.set_state(Repository.STATE_CREATED)
274 274
275 275 repo_id = repo.repo_id
276 276 repo_data = repo.get_api_data()
277 audit_logger.store(
277 audit_logger.store_web(
278 278 action='repo.fork',
279 279 action_data={'data': repo_data},
280 280 user=cur_user,
281 281 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
282 282
283 283 DBS.commit()
284 284 except Exception as e:
285 285 log.warning('Exception %s occurred when forking repository, '
286 286 'doing cleanup...', e)
287 287 # rollback things manually !
288 288 repo = Repository.get_by_repo_name(repo_name_full)
289 289 if repo:
290 290 Repository.delete(repo.repo_id)
291 291 DBS.commit()
292 292 RepoModel(DBS)._delete_filesystem_repo(repo)
293 293 raise
294 294
295 295 # it's an odd fix to make celery fail task when exception occurs
296 296 def on_failure(self, *args, **kwargs):
297 297 pass
298 298
299 299 return True
General Comments 0
You need to be logged in to leave comments. Login now