##// END OF EJS Templates
pylons: fixed code and test suite after removal of pylons.
marcink -
r2358:d7106a21 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,63 +1,63 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 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 # link to config for pylons
43 # link to config for pyramid
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 54 __dbversion__ = 81 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'https://code.rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
63 63 disable_error_handler = False
@@ -1,2066 +1,2067 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'] = cs._get_refs()
344 344 return _cs_json
345 345
346 346
347 347 @jsonrpc_method()
348 348 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
349 349 details=Optional('basic')):
350 350 """
351 351 Returns a set of commits limited by the number starting
352 352 from the `start_rev` option.
353 353
354 354 Additional parameters define the amount of details returned by this
355 355 function.
356 356
357 357 This command can only be run using an |authtoken| with admin rights,
358 358 or users with at least read rights to |repos|.
359 359
360 360 :param apiuser: This is filled automatically from the |authtoken|.
361 361 :type apiuser: AuthUser
362 362 :param repoid: The repository name or repository ID.
363 363 :type repoid: str or int
364 364 :param start_rev: The starting revision from where to get changesets.
365 365 :type start_rev: str
366 366 :param limit: Limit the number of commits to this amount
367 367 :type limit: str or int
368 368 :param details: Set the level of detail returned. Valid option are:
369 369 ``basic``, ``extended`` and ``full``.
370 370 :type details: Optional(str)
371 371
372 372 .. note::
373 373
374 374 Setting the parameter `details` to the value ``full`` is extensive
375 375 and returns details like the diff itself, and the number
376 376 of changed files.
377 377
378 378 """
379 379 repo = get_repo_or_error(repoid)
380 380 if not has_superadmin_permission(apiuser):
381 381 _perms = (
382 382 'repository.admin', 'repository.write', 'repository.read',)
383 383 validate_repo_permissions(apiuser, repoid, repo, _perms)
384 384
385 385 changes_details = Optional.extract(details)
386 386 _changes_details_types = ['basic', 'extended', 'full']
387 387 if changes_details not in _changes_details_types:
388 388 raise JSONRPCError(
389 389 'ret_type must be one of %s' % (
390 390 ','.join(_changes_details_types)))
391 391
392 392 limit = int(limit)
393 393 pre_load = ['author', 'branch', 'date', 'message', 'parents',
394 394 'status', '_commit', '_file_paths']
395 395
396 396 vcs_repo = repo.scm_instance()
397 397 # SVN needs a special case to distinguish its index and commit id
398 398 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
399 399 start_rev = vcs_repo.commit_ids[0]
400 400
401 401 try:
402 402 commits = vcs_repo.get_commits(
403 403 start_id=start_rev, pre_load=pre_load)
404 404 except TypeError as e:
405 405 raise JSONRPCError(e.message)
406 406 except Exception:
407 407 log.exception('Fetching of commits failed')
408 408 raise JSONRPCError('Error occurred during commit fetching')
409 409
410 410 ret = []
411 411 for cnt, commit in enumerate(commits):
412 412 if cnt >= limit != -1:
413 413 break
414 414 _cs_json = commit.__json__()
415 415 _cs_json['diff'] = build_commit_data(commit, changes_details)
416 416 if changes_details == 'full':
417 417 _cs_json['refs'] = {
418 418 'branches': [commit.branch],
419 419 'bookmarks': getattr(commit, 'bookmarks', []),
420 420 'tags': commit.tags
421 421 }
422 422 ret.append(_cs_json)
423 423 return ret
424 424
425 425
426 426 @jsonrpc_method()
427 427 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
428 428 ret_type=Optional('all'), details=Optional('basic'),
429 429 max_file_bytes=Optional(None)):
430 430 """
431 431 Returns a list of nodes and children in a flat list for a given
432 432 path at given revision.
433 433
434 434 It's possible to specify ret_type to show only `files` or `dirs`.
435 435
436 436 This command can only be run using an |authtoken| with admin rights,
437 437 or users with at least read rights to |repos|.
438 438
439 439 :param apiuser: This is filled automatically from the |authtoken|.
440 440 :type apiuser: AuthUser
441 441 :param repoid: The repository name or repository ID.
442 442 :type repoid: str or int
443 443 :param revision: The revision for which listing should be done.
444 444 :type revision: str
445 445 :param root_path: The path from which to start displaying.
446 446 :type root_path: str
447 447 :param ret_type: Set the return type. Valid options are
448 448 ``all`` (default), ``files`` and ``dirs``.
449 449 :type ret_type: Optional(str)
450 450 :param details: Returns extended information about nodes, such as
451 451 md5, binary, and or content. The valid options are ``basic`` and
452 452 ``full``.
453 453 :type details: Optional(str)
454 454 :param max_file_bytes: Only return file content under this file size bytes
455 455 :type details: Optional(int)
456 456
457 457 Example output:
458 458
459 459 .. code-block:: bash
460 460
461 461 id : <id_given_in_input>
462 462 result: [
463 463 {
464 464 "name" : "<name>"
465 465 "type" : "<type>",
466 466 "binary": "<true|false>" (only in extended mode)
467 467 "md5" : "<md5 of file content>" (only in extended mode)
468 468 },
469 469 ...
470 470 ]
471 471 error: null
472 472 """
473 473
474 474 repo = get_repo_or_error(repoid)
475 475 if not has_superadmin_permission(apiuser):
476 476 _perms = (
477 477 'repository.admin', 'repository.write', 'repository.read',)
478 478 validate_repo_permissions(apiuser, repoid, repo, _perms)
479 479
480 480 ret_type = Optional.extract(ret_type)
481 481 details = Optional.extract(details)
482 482 _extended_types = ['basic', 'full']
483 483 if details not in _extended_types:
484 484 raise JSONRPCError(
485 485 'ret_type must be one of %s' % (','.join(_extended_types)))
486 486 extended_info = False
487 487 content = False
488 488 if details == 'basic':
489 489 extended_info = True
490 490
491 491 if details == 'full':
492 492 extended_info = content = True
493 493
494 494 _map = {}
495 495 try:
496 496 # check if repo is not empty by any chance, skip quicker if it is.
497 497 _scm = repo.scm_instance()
498 498 if _scm.is_empty():
499 499 return []
500 500
501 501 _d, _f = ScmModel().get_nodes(
502 502 repo, revision, root_path, flat=False,
503 503 extended_info=extended_info, content=content,
504 504 max_file_bytes=max_file_bytes)
505 505 _map = {
506 506 'all': _d + _f,
507 507 'files': _f,
508 508 'dirs': _d,
509 509 }
510 510 return _map[ret_type]
511 511 except KeyError:
512 512 raise JSONRPCError(
513 513 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
514 514 except Exception:
515 515 log.exception("Exception occurred while trying to get repo nodes")
516 516 raise JSONRPCError(
517 517 'failed to get repo: `%s` nodes' % repo.repo_name
518 518 )
519 519
520 520
521 521 @jsonrpc_method()
522 522 def get_repo_refs(request, apiuser, repoid):
523 523 """
524 524 Returns a dictionary of current references. It returns
525 525 bookmarks, branches, closed_branches, and tags for given repository
526 526
527 527 It's possible to specify ret_type to show only `files` or `dirs`.
528 528
529 529 This command can only be run using an |authtoken| with admin rights,
530 530 or users with at least read rights to |repos|.
531 531
532 532 :param apiuser: This is filled automatically from the |authtoken|.
533 533 :type apiuser: AuthUser
534 534 :param repoid: The repository name or repository ID.
535 535 :type repoid: str or int
536 536
537 537 Example output:
538 538
539 539 .. code-block:: bash
540 540
541 541 id : <id_given_in_input>
542 542 "result": {
543 543 "bookmarks": {
544 544 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
545 545 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
546 546 },
547 547 "branches": {
548 548 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
549 549 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
550 550 },
551 551 "branches_closed": {},
552 552 "tags": {
553 553 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
554 554 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
555 555 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
556 556 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
557 557 }
558 558 }
559 559 error: null
560 560 """
561 561
562 562 repo = get_repo_or_error(repoid)
563 563 if not has_superadmin_permission(apiuser):
564 564 _perms = ('repository.admin', 'repository.write', 'repository.read',)
565 565 validate_repo_permissions(apiuser, repoid, repo, _perms)
566 566
567 567 try:
568 568 # check if repo is not empty by any chance, skip quicker if it is.
569 569 vcs_instance = repo.scm_instance()
570 570 refs = vcs_instance.refs()
571 571 return refs
572 572 except Exception:
573 573 log.exception("Exception occurred while trying to get repo refs")
574 574 raise JSONRPCError(
575 575 'failed to get repo: `%s` references' % repo.repo_name
576 576 )
577 577
578 578
579 579 @jsonrpc_method()
580 580 def create_repo(
581 581 request, apiuser, repo_name, repo_type,
582 582 owner=Optional(OAttr('apiuser')),
583 583 description=Optional(''),
584 584 private=Optional(False),
585 585 clone_uri=Optional(None),
586 586 landing_rev=Optional('rev:tip'),
587 587 enable_statistics=Optional(False),
588 588 enable_locking=Optional(False),
589 589 enable_downloads=Optional(False),
590 590 copy_permissions=Optional(False)):
591 591 """
592 592 Creates a repository.
593 593
594 594 * If the repository name contains "/", repository will be created inside
595 595 a repository group or nested repository groups
596 596
597 597 For example "foo/bar/repo1" will create |repo| called "repo1" inside
598 598 group "foo/bar". You have to have permissions to access and write to
599 599 the last repository group ("bar" in this example)
600 600
601 601 This command can only be run using an |authtoken| with at least
602 602 permissions to create repositories, or write permissions to
603 603 parent repository groups.
604 604
605 605 :param apiuser: This is filled automatically from the |authtoken|.
606 606 :type apiuser: AuthUser
607 607 :param repo_name: Set the repository name.
608 608 :type repo_name: str
609 609 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
610 610 :type repo_type: str
611 611 :param owner: user_id or username
612 612 :type owner: Optional(str)
613 613 :param description: Set the repository description.
614 614 :type description: Optional(str)
615 615 :param private: set repository as private
616 616 :type private: bool
617 617 :param clone_uri: set clone_uri
618 618 :type clone_uri: str
619 619 :param landing_rev: <rev_type>:<rev>
620 620 :type landing_rev: str
621 621 :param enable_locking:
622 622 :type enable_locking: bool
623 623 :param enable_downloads:
624 624 :type enable_downloads: bool
625 625 :param enable_statistics:
626 626 :type enable_statistics: bool
627 627 :param copy_permissions: Copy permission from group in which the
628 628 repository is being created.
629 629 :type copy_permissions: bool
630 630
631 631
632 632 Example output:
633 633
634 634 .. code-block:: bash
635 635
636 636 id : <id_given_in_input>
637 637 result: {
638 638 "msg": "Created new repository `<reponame>`",
639 639 "success": true,
640 640 "task": "<celery task id or None if done sync>"
641 641 }
642 642 error: null
643 643
644 644
645 645 Example error output:
646 646
647 647 .. code-block:: bash
648 648
649 649 id : <id_given_in_input>
650 650 result : null
651 651 error : {
652 652 'failed to create repository `<repo_name>`'
653 653 }
654 654
655 655 """
656 656
657 657 owner = validate_set_owner_permissions(apiuser, owner)
658 658
659 659 description = Optional.extract(description)
660 660 copy_permissions = Optional.extract(copy_permissions)
661 661 clone_uri = Optional.extract(clone_uri)
662 662 landing_commit_ref = Optional.extract(landing_rev)
663 663
664 664 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
665 665 if isinstance(private, Optional):
666 666 private = defs.get('repo_private') or Optional.extract(private)
667 667 if isinstance(repo_type, Optional):
668 668 repo_type = defs.get('repo_type')
669 669 if isinstance(enable_statistics, Optional):
670 670 enable_statistics = defs.get('repo_enable_statistics')
671 671 if isinstance(enable_locking, Optional):
672 672 enable_locking = defs.get('repo_enable_locking')
673 673 if isinstance(enable_downloads, Optional):
674 674 enable_downloads = defs.get('repo_enable_downloads')
675 675
676 676 schema = repo_schema.RepoSchema().bind(
677 677 repo_type_options=rhodecode.BACKENDS.keys(),
678 678 # user caller
679 679 user=apiuser)
680 680
681 681 try:
682 682 schema_data = schema.deserialize(dict(
683 683 repo_name=repo_name,
684 684 repo_type=repo_type,
685 685 repo_owner=owner.username,
686 686 repo_description=description,
687 687 repo_landing_commit_ref=landing_commit_ref,
688 688 repo_clone_uri=clone_uri,
689 689 repo_private=private,
690 690 repo_copy_permissions=copy_permissions,
691 691 repo_enable_statistics=enable_statistics,
692 692 repo_enable_downloads=enable_downloads,
693 693 repo_enable_locking=enable_locking))
694 694 except validation_schema.Invalid as err:
695 695 raise JSONRPCValidationError(colander_exc=err)
696 696
697 697 try:
698 698 data = {
699 699 'owner': owner,
700 700 'repo_name': schema_data['repo_group']['repo_name_without_group'],
701 701 'repo_name_full': schema_data['repo_name'],
702 702 'repo_group': schema_data['repo_group']['repo_group_id'],
703 703 'repo_type': schema_data['repo_type'],
704 704 'repo_description': schema_data['repo_description'],
705 705 'repo_private': schema_data['repo_private'],
706 706 'clone_uri': schema_data['repo_clone_uri'],
707 707 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
708 708 'enable_statistics': schema_data['repo_enable_statistics'],
709 709 'enable_locking': schema_data['repo_enable_locking'],
710 710 'enable_downloads': schema_data['repo_enable_downloads'],
711 711 'repo_copy_permissions': schema_data['repo_copy_permissions'],
712 712 }
713 713
714 714 task = RepoModel().create(form_data=data, cur_user=owner)
715 715 from celery.result import BaseAsyncResult
716 716 task_id = None
717 717 if isinstance(task, BaseAsyncResult):
718 718 task_id = task.task_id
719 719 # no commit, it's done in RepoModel, or async via celery
720 720 return {
721 721 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
722 722 'success': True, # cannot return the repo data here since fork
723 723 # can be done async
724 724 'task': task_id
725 725 }
726 726 except Exception:
727 727 log.exception(
728 728 u"Exception while trying to create the repository %s",
729 729 schema_data['repo_name'])
730 730 raise JSONRPCError(
731 731 'failed to create repository `%s`' % (schema_data['repo_name'],))
732 732
733 733
734 734 @jsonrpc_method()
735 735 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
736 736 description=Optional('')):
737 737 """
738 738 Adds an extra field to a repository.
739 739
740 740 This command can only be run using an |authtoken| with at least
741 741 write permissions to the |repo|.
742 742
743 743 :param apiuser: This is filled automatically from the |authtoken|.
744 744 :type apiuser: AuthUser
745 745 :param repoid: Set the repository name or repository id.
746 746 :type repoid: str or int
747 747 :param key: Create a unique field key for this repository.
748 748 :type key: str
749 749 :param label:
750 750 :type label: Optional(str)
751 751 :param description:
752 752 :type description: Optional(str)
753 753 """
754 754 repo = get_repo_or_error(repoid)
755 755 if not has_superadmin_permission(apiuser):
756 756 _perms = ('repository.admin',)
757 757 validate_repo_permissions(apiuser, repoid, repo, _perms)
758 758
759 759 label = Optional.extract(label) or key
760 760 description = Optional.extract(description)
761 761
762 762 field = RepositoryField.get_by_key_name(key, repo)
763 763 if field:
764 764 raise JSONRPCError('Field with key '
765 765 '`%s` exists for repo `%s`' % (key, repoid))
766 766
767 767 try:
768 768 RepoModel().add_repo_field(repo, key, field_label=label,
769 769 field_desc=description)
770 770 Session().commit()
771 771 return {
772 772 'msg': "Added new repository field `%s`" % (key,),
773 773 'success': True,
774 774 }
775 775 except Exception:
776 776 log.exception("Exception occurred while trying to add field to repo")
777 777 raise JSONRPCError(
778 778 'failed to create new field for repository `%s`' % (repoid,))
779 779
780 780
781 781 @jsonrpc_method()
782 782 def remove_field_from_repo(request, apiuser, repoid, key):
783 783 """
784 784 Removes an extra field from a repository.
785 785
786 786 This command can only be run using an |authtoken| with at least
787 787 write permissions to the |repo|.
788 788
789 789 :param apiuser: This is filled automatically from the |authtoken|.
790 790 :type apiuser: AuthUser
791 791 :param repoid: Set the repository name or repository ID.
792 792 :type repoid: str or int
793 793 :param key: Set the unique field key for this repository.
794 794 :type key: str
795 795 """
796 796
797 797 repo = get_repo_or_error(repoid)
798 798 if not has_superadmin_permission(apiuser):
799 799 _perms = ('repository.admin',)
800 800 validate_repo_permissions(apiuser, repoid, repo, _perms)
801 801
802 802 field = RepositoryField.get_by_key_name(key, repo)
803 803 if not field:
804 804 raise JSONRPCError('Field with key `%s` does not '
805 805 'exists for repo `%s`' % (key, repoid))
806 806
807 807 try:
808 808 RepoModel().delete_repo_field(repo, field_key=key)
809 809 Session().commit()
810 810 return {
811 811 'msg': "Deleted repository field `%s`" % (key,),
812 812 'success': True,
813 813 }
814 814 except Exception:
815 815 log.exception(
816 816 "Exception occurred while trying to delete field from repo")
817 817 raise JSONRPCError(
818 818 'failed to delete field for repository `%s`' % (repoid,))
819 819
820 820
821 821 @jsonrpc_method()
822 822 def update_repo(
823 823 request, apiuser, repoid, repo_name=Optional(None),
824 824 owner=Optional(OAttr('apiuser')), description=Optional(''),
825 825 private=Optional(False), clone_uri=Optional(None),
826 826 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
827 827 enable_statistics=Optional(False),
828 828 enable_locking=Optional(False),
829 829 enable_downloads=Optional(False), fields=Optional('')):
830 830 """
831 831 Updates a repository with the given information.
832 832
833 833 This command can only be run using an |authtoken| with at least
834 834 admin permissions to the |repo|.
835 835
836 836 * If the repository name contains "/", repository will be updated
837 837 accordingly with a repository group or nested repository groups
838 838
839 839 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
840 840 called "repo-test" and place it inside group "foo/bar".
841 841 You have to have permissions to access and write to the last repository
842 842 group ("bar" in this example)
843 843
844 844 :param apiuser: This is filled automatically from the |authtoken|.
845 845 :type apiuser: AuthUser
846 846 :param repoid: repository name or repository ID.
847 847 :type repoid: str or int
848 848 :param repo_name: Update the |repo| name, including the
849 849 repository group it's in.
850 850 :type repo_name: str
851 851 :param owner: Set the |repo| owner.
852 852 :type owner: str
853 853 :param fork_of: Set the |repo| as fork of another |repo|.
854 854 :type fork_of: str
855 855 :param description: Update the |repo| description.
856 856 :type description: str
857 857 :param private: Set the |repo| as private. (True | False)
858 858 :type private: bool
859 859 :param clone_uri: Update the |repo| clone URI.
860 860 :type clone_uri: str
861 861 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
862 862 :type landing_rev: str
863 863 :param enable_statistics: Enable statistics on the |repo|, (True | False).
864 864 :type enable_statistics: bool
865 865 :param enable_locking: Enable |repo| locking.
866 866 :type enable_locking: bool
867 867 :param enable_downloads: Enable downloads from the |repo|, (True | False).
868 868 :type enable_downloads: bool
869 869 :param fields: Add extra fields to the |repo|. Use the following
870 870 example format: ``field_key=field_val,field_key2=fieldval2``.
871 871 Escape ', ' with \,
872 872 :type fields: str
873 873 """
874 874
875 875 repo = get_repo_or_error(repoid)
876 876
877 877 include_secrets = False
878 878 if not has_superadmin_permission(apiuser):
879 879 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
880 880 else:
881 881 include_secrets = True
882 882
883 883 updates = dict(
884 884 repo_name=repo_name
885 885 if not isinstance(repo_name, Optional) else repo.repo_name,
886 886
887 887 fork_id=fork_of
888 888 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
889 889
890 890 user=owner
891 891 if not isinstance(owner, Optional) else repo.user.username,
892 892
893 893 repo_description=description
894 894 if not isinstance(description, Optional) else repo.description,
895 895
896 896 repo_private=private
897 897 if not isinstance(private, Optional) else repo.private,
898 898
899 899 clone_uri=clone_uri
900 900 if not isinstance(clone_uri, Optional) else repo.clone_uri,
901 901
902 902 repo_landing_rev=landing_rev
903 903 if not isinstance(landing_rev, Optional) else repo._landing_revision,
904 904
905 905 repo_enable_statistics=enable_statistics
906 906 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
907 907
908 908 repo_enable_locking=enable_locking
909 909 if not isinstance(enable_locking, Optional) else repo.enable_locking,
910 910
911 911 repo_enable_downloads=enable_downloads
912 912 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
913 913
914 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
914 ref_choices, _labels = ScmModel().get_repo_landing_revs(
915 request.translate, repo=repo)
915 916
916 917 old_values = repo.get_api_data()
917 918 schema = repo_schema.RepoSchema().bind(
918 919 repo_type_options=rhodecode.BACKENDS.keys(),
919 920 repo_ref_options=ref_choices,
920 921 # user caller
921 922 user=apiuser,
922 923 old_values=old_values)
923 924 try:
924 925 schema_data = schema.deserialize(dict(
925 926 # we save old value, users cannot change type
926 927 repo_type=repo.repo_type,
927 928
928 929 repo_name=updates['repo_name'],
929 930 repo_owner=updates['user'],
930 931 repo_description=updates['repo_description'],
931 932 repo_clone_uri=updates['clone_uri'],
932 933 repo_fork_of=updates['fork_id'],
933 934 repo_private=updates['repo_private'],
934 935 repo_landing_commit_ref=updates['repo_landing_rev'],
935 936 repo_enable_statistics=updates['repo_enable_statistics'],
936 937 repo_enable_downloads=updates['repo_enable_downloads'],
937 938 repo_enable_locking=updates['repo_enable_locking']))
938 939 except validation_schema.Invalid as err:
939 940 raise JSONRPCValidationError(colander_exc=err)
940 941
941 942 # save validated data back into the updates dict
942 943 validated_updates = dict(
943 944 repo_name=schema_data['repo_group']['repo_name_without_group'],
944 945 repo_group=schema_data['repo_group']['repo_group_id'],
945 946
946 947 user=schema_data['repo_owner'],
947 948 repo_description=schema_data['repo_description'],
948 949 repo_private=schema_data['repo_private'],
949 950 clone_uri=schema_data['repo_clone_uri'],
950 951 repo_landing_rev=schema_data['repo_landing_commit_ref'],
951 952 repo_enable_statistics=schema_data['repo_enable_statistics'],
952 953 repo_enable_locking=schema_data['repo_enable_locking'],
953 954 repo_enable_downloads=schema_data['repo_enable_downloads'],
954 955 )
955 956
956 957 if schema_data['repo_fork_of']:
957 958 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
958 959 validated_updates['fork_id'] = fork_repo.repo_id
959 960
960 961 # extra fields
961 962 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
962 963 if fields:
963 964 validated_updates.update(fields)
964 965
965 966 try:
966 967 RepoModel().update(repo, **validated_updates)
967 968 audit_logger.store_api(
968 969 'repo.edit', action_data={'old_data': old_values},
969 970 user=apiuser, repo=repo)
970 971 Session().commit()
971 972 return {
972 973 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
973 974 'repository': repo.get_api_data(include_secrets=include_secrets)
974 975 }
975 976 except Exception:
976 977 log.exception(
977 978 u"Exception while trying to update the repository %s",
978 979 repoid)
979 980 raise JSONRPCError('failed to update repo `%s`' % repoid)
980 981
981 982
982 983 @jsonrpc_method()
983 984 def fork_repo(request, apiuser, repoid, fork_name,
984 985 owner=Optional(OAttr('apiuser')),
985 986 description=Optional(''),
986 987 private=Optional(False),
987 988 clone_uri=Optional(None),
988 989 landing_rev=Optional('rev:tip'),
989 990 copy_permissions=Optional(False)):
990 991 """
991 992 Creates a fork of the specified |repo|.
992 993
993 994 * If the fork_name contains "/", fork will be created inside
994 995 a repository group or nested repository groups
995 996
996 997 For example "foo/bar/fork-repo" will create fork called "fork-repo"
997 998 inside group "foo/bar". You have to have permissions to access and
998 999 write to the last repository group ("bar" in this example)
999 1000
1000 1001 This command can only be run using an |authtoken| with minimum
1001 1002 read permissions of the forked repo, create fork permissions for an user.
1002 1003
1003 1004 :param apiuser: This is filled automatically from the |authtoken|.
1004 1005 :type apiuser: AuthUser
1005 1006 :param repoid: Set repository name or repository ID.
1006 1007 :type repoid: str or int
1007 1008 :param fork_name: Set the fork name, including it's repository group membership.
1008 1009 :type fork_name: str
1009 1010 :param owner: Set the fork owner.
1010 1011 :type owner: str
1011 1012 :param description: Set the fork description.
1012 1013 :type description: str
1013 1014 :param copy_permissions: Copy permissions from parent |repo|. The
1014 1015 default is False.
1015 1016 :type copy_permissions: bool
1016 1017 :param private: Make the fork private. The default is False.
1017 1018 :type private: bool
1018 1019 :param landing_rev: Set the landing revision. The default is tip.
1019 1020
1020 1021 Example output:
1021 1022
1022 1023 .. code-block:: bash
1023 1024
1024 1025 id : <id_for_response>
1025 1026 api_key : "<api_key>"
1026 1027 args: {
1027 1028 "repoid" : "<reponame or repo_id>",
1028 1029 "fork_name": "<forkname>",
1029 1030 "owner": "<username or user_id = Optional(=apiuser)>",
1030 1031 "description": "<description>",
1031 1032 "copy_permissions": "<bool>",
1032 1033 "private": "<bool>",
1033 1034 "landing_rev": "<landing_rev>"
1034 1035 }
1035 1036
1036 1037 Example error output:
1037 1038
1038 1039 .. code-block:: bash
1039 1040
1040 1041 id : <id_given_in_input>
1041 1042 result: {
1042 1043 "msg": "Created fork of `<reponame>` as `<forkname>`",
1043 1044 "success": true,
1044 1045 "task": "<celery task id or None if done sync>"
1045 1046 }
1046 1047 error: null
1047 1048
1048 1049 """
1049 1050
1050 1051 repo = get_repo_or_error(repoid)
1051 1052 repo_name = repo.repo_name
1052 1053
1053 1054 if not has_superadmin_permission(apiuser):
1054 1055 # check if we have at least read permission for
1055 1056 # this repo that we fork !
1056 1057 _perms = (
1057 1058 'repository.admin', 'repository.write', 'repository.read')
1058 1059 validate_repo_permissions(apiuser, repoid, repo, _perms)
1059 1060
1060 1061 # check if the regular user has at least fork permissions as well
1061 1062 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1062 1063 raise JSONRPCForbidden()
1063 1064
1064 1065 # check if user can set owner parameter
1065 1066 owner = validate_set_owner_permissions(apiuser, owner)
1066 1067
1067 1068 description = Optional.extract(description)
1068 1069 copy_permissions = Optional.extract(copy_permissions)
1069 1070 clone_uri = Optional.extract(clone_uri)
1070 1071 landing_commit_ref = Optional.extract(landing_rev)
1071 1072 private = Optional.extract(private)
1072 1073
1073 1074 schema = repo_schema.RepoSchema().bind(
1074 1075 repo_type_options=rhodecode.BACKENDS.keys(),
1075 1076 # user caller
1076 1077 user=apiuser)
1077 1078
1078 1079 try:
1079 1080 schema_data = schema.deserialize(dict(
1080 1081 repo_name=fork_name,
1081 1082 repo_type=repo.repo_type,
1082 1083 repo_owner=owner.username,
1083 1084 repo_description=description,
1084 1085 repo_landing_commit_ref=landing_commit_ref,
1085 1086 repo_clone_uri=clone_uri,
1086 1087 repo_private=private,
1087 1088 repo_copy_permissions=copy_permissions))
1088 1089 except validation_schema.Invalid as err:
1089 1090 raise JSONRPCValidationError(colander_exc=err)
1090 1091
1091 1092 try:
1092 1093 data = {
1093 1094 'fork_parent_id': repo.repo_id,
1094 1095
1095 1096 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1096 1097 'repo_name_full': schema_data['repo_name'],
1097 1098 'repo_group': schema_data['repo_group']['repo_group_id'],
1098 1099 'repo_type': schema_data['repo_type'],
1099 1100 'description': schema_data['repo_description'],
1100 1101 'private': schema_data['repo_private'],
1101 1102 'copy_permissions': schema_data['repo_copy_permissions'],
1102 1103 'landing_rev': schema_data['repo_landing_commit_ref'],
1103 1104 }
1104 1105
1105 1106 task = RepoModel().create_fork(data, cur_user=owner)
1106 1107 # no commit, it's done in RepoModel, or async via celery
1107 1108 from celery.result import BaseAsyncResult
1108 1109 task_id = None
1109 1110 if isinstance(task, BaseAsyncResult):
1110 1111 task_id = task.task_id
1111 1112 return {
1112 1113 'msg': 'Created fork of `%s` as `%s`' % (
1113 1114 repo.repo_name, schema_data['repo_name']),
1114 1115 'success': True, # cannot return the repo data here since fork
1115 1116 # can be done async
1116 1117 'task': task_id
1117 1118 }
1118 1119 except Exception:
1119 1120 log.exception(
1120 1121 u"Exception while trying to create fork %s",
1121 1122 schema_data['repo_name'])
1122 1123 raise JSONRPCError(
1123 1124 'failed to fork repository `%s` as `%s`' % (
1124 1125 repo_name, schema_data['repo_name']))
1125 1126
1126 1127
1127 1128 @jsonrpc_method()
1128 1129 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1129 1130 """
1130 1131 Deletes a repository.
1131 1132
1132 1133 * When the `forks` parameter is set it's possible to detach or delete
1133 1134 forks of deleted repository.
1134 1135
1135 1136 This command can only be run using an |authtoken| with admin
1136 1137 permissions on the |repo|.
1137 1138
1138 1139 :param apiuser: This is filled automatically from the |authtoken|.
1139 1140 :type apiuser: AuthUser
1140 1141 :param repoid: Set the repository name or repository ID.
1141 1142 :type repoid: str or int
1142 1143 :param forks: Set to `detach` or `delete` forks from the |repo|.
1143 1144 :type forks: Optional(str)
1144 1145
1145 1146 Example error output:
1146 1147
1147 1148 .. code-block:: bash
1148 1149
1149 1150 id : <id_given_in_input>
1150 1151 result: {
1151 1152 "msg": "Deleted repository `<reponame>`",
1152 1153 "success": true
1153 1154 }
1154 1155 error: null
1155 1156 """
1156 1157
1157 1158 repo = get_repo_or_error(repoid)
1158 1159 repo_name = repo.repo_name
1159 1160 if not has_superadmin_permission(apiuser):
1160 1161 _perms = ('repository.admin',)
1161 1162 validate_repo_permissions(apiuser, repoid, repo, _perms)
1162 1163
1163 1164 try:
1164 1165 handle_forks = Optional.extract(forks)
1165 1166 _forks_msg = ''
1166 1167 _forks = [f for f in repo.forks]
1167 1168 if handle_forks == 'detach':
1168 1169 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1169 1170 elif handle_forks == 'delete':
1170 1171 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1171 1172 elif _forks:
1172 1173 raise JSONRPCError(
1173 1174 'Cannot delete `%s` it still contains attached forks' %
1174 1175 (repo.repo_name,)
1175 1176 )
1176 1177 old_data = repo.get_api_data()
1177 1178 RepoModel().delete(repo, forks=forks)
1178 1179
1179 1180 repo = audit_logger.RepoWrap(repo_id=None,
1180 1181 repo_name=repo.repo_name)
1181 1182
1182 1183 audit_logger.store_api(
1183 1184 'repo.delete', action_data={'old_data': old_data},
1184 1185 user=apiuser, repo=repo)
1185 1186
1186 1187 ScmModel().mark_for_invalidation(repo_name, delete=True)
1187 1188 Session().commit()
1188 1189 return {
1189 1190 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1190 1191 'success': True
1191 1192 }
1192 1193 except Exception:
1193 1194 log.exception("Exception occurred while trying to delete repo")
1194 1195 raise JSONRPCError(
1195 1196 'failed to delete repository `%s`' % (repo_name,)
1196 1197 )
1197 1198
1198 1199
1199 1200 #TODO: marcink, change name ?
1200 1201 @jsonrpc_method()
1201 1202 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1202 1203 """
1203 1204 Invalidates the cache for the specified repository.
1204 1205
1205 1206 This command can only be run using an |authtoken| with admin rights to
1206 1207 the specified repository.
1207 1208
1208 1209 This command takes the following options:
1209 1210
1210 1211 :param apiuser: This is filled automatically from |authtoken|.
1211 1212 :type apiuser: AuthUser
1212 1213 :param repoid: Sets the repository name or repository ID.
1213 1214 :type repoid: str or int
1214 1215 :param delete_keys: This deletes the invalidated keys instead of
1215 1216 just flagging them.
1216 1217 :type delete_keys: Optional(``True`` | ``False``)
1217 1218
1218 1219 Example output:
1219 1220
1220 1221 .. code-block:: bash
1221 1222
1222 1223 id : <id_given_in_input>
1223 1224 result : {
1224 1225 'msg': Cache for repository `<repository name>` was invalidated,
1225 1226 'repository': <repository name>
1226 1227 }
1227 1228 error : null
1228 1229
1229 1230 Example error output:
1230 1231
1231 1232 .. code-block:: bash
1232 1233
1233 1234 id : <id_given_in_input>
1234 1235 result : null
1235 1236 error : {
1236 1237 'Error occurred during cache invalidation action'
1237 1238 }
1238 1239
1239 1240 """
1240 1241
1241 1242 repo = get_repo_or_error(repoid)
1242 1243 if not has_superadmin_permission(apiuser):
1243 1244 _perms = ('repository.admin', 'repository.write',)
1244 1245 validate_repo_permissions(apiuser, repoid, repo, _perms)
1245 1246
1246 1247 delete = Optional.extract(delete_keys)
1247 1248 try:
1248 1249 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1249 1250 return {
1250 1251 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1251 1252 'repository': repo.repo_name
1252 1253 }
1253 1254 except Exception:
1254 1255 log.exception(
1255 1256 "Exception occurred while trying to invalidate repo cache")
1256 1257 raise JSONRPCError(
1257 1258 'Error occurred during cache invalidation action'
1258 1259 )
1259 1260
1260 1261
1261 1262 #TODO: marcink, change name ?
1262 1263 @jsonrpc_method()
1263 1264 def lock(request, apiuser, repoid, locked=Optional(None),
1264 1265 userid=Optional(OAttr('apiuser'))):
1265 1266 """
1266 1267 Sets the lock state of the specified |repo| by the given user.
1267 1268 From more information, see :ref:`repo-locking`.
1268 1269
1269 1270 * If the ``userid`` option is not set, the repository is locked to the
1270 1271 user who called the method.
1271 1272 * If the ``locked`` parameter is not set, the current lock state of the
1272 1273 repository is displayed.
1273 1274
1274 1275 This command can only be run using an |authtoken| with admin rights to
1275 1276 the specified repository.
1276 1277
1277 1278 This command takes the following options:
1278 1279
1279 1280 :param apiuser: This is filled automatically from the |authtoken|.
1280 1281 :type apiuser: AuthUser
1281 1282 :param repoid: Sets the repository name or repository ID.
1282 1283 :type repoid: str or int
1283 1284 :param locked: Sets the lock state.
1284 1285 :type locked: Optional(``True`` | ``False``)
1285 1286 :param userid: Set the repository lock to this user.
1286 1287 :type userid: Optional(str or int)
1287 1288
1288 1289 Example error output:
1289 1290
1290 1291 .. code-block:: bash
1291 1292
1292 1293 id : <id_given_in_input>
1293 1294 result : {
1294 1295 'repo': '<reponame>',
1295 1296 'locked': <bool: lock state>,
1296 1297 'locked_since': <int: lock timestamp>,
1297 1298 'locked_by': <username of person who made the lock>,
1298 1299 'lock_reason': <str: reason for locking>,
1299 1300 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1300 1301 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1301 1302 or
1302 1303 'msg': 'Repo `<repository name>` not locked.'
1303 1304 or
1304 1305 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1305 1306 }
1306 1307 error : null
1307 1308
1308 1309 Example error output:
1309 1310
1310 1311 .. code-block:: bash
1311 1312
1312 1313 id : <id_given_in_input>
1313 1314 result : null
1314 1315 error : {
1315 1316 'Error occurred locking repository `<reponame>`'
1316 1317 }
1317 1318 """
1318 1319
1319 1320 repo = get_repo_or_error(repoid)
1320 1321 if not has_superadmin_permission(apiuser):
1321 1322 # check if we have at least write permission for this repo !
1322 1323 _perms = ('repository.admin', 'repository.write',)
1323 1324 validate_repo_permissions(apiuser, repoid, repo, _perms)
1324 1325
1325 1326 # make sure normal user does not pass someone else userid,
1326 1327 # he is not allowed to do that
1327 1328 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1328 1329 raise JSONRPCError('userid is not the same as your user')
1329 1330
1330 1331 if isinstance(userid, Optional):
1331 1332 userid = apiuser.user_id
1332 1333
1333 1334 user = get_user_or_error(userid)
1334 1335
1335 1336 if isinstance(locked, Optional):
1336 1337 lockobj = repo.locked
1337 1338
1338 1339 if lockobj[0] is None:
1339 1340 _d = {
1340 1341 'repo': repo.repo_name,
1341 1342 'locked': False,
1342 1343 'locked_since': None,
1343 1344 'locked_by': None,
1344 1345 'lock_reason': None,
1345 1346 'lock_state_changed': False,
1346 1347 'msg': 'Repo `%s` not locked.' % repo.repo_name
1347 1348 }
1348 1349 return _d
1349 1350 else:
1350 1351 _user_id, _time, _reason = lockobj
1351 1352 lock_user = get_user_or_error(userid)
1352 1353 _d = {
1353 1354 'repo': repo.repo_name,
1354 1355 'locked': True,
1355 1356 'locked_since': _time,
1356 1357 'locked_by': lock_user.username,
1357 1358 'lock_reason': _reason,
1358 1359 'lock_state_changed': False,
1359 1360 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1360 1361 % (repo.repo_name, lock_user.username,
1361 1362 json.dumps(time_to_datetime(_time))))
1362 1363 }
1363 1364 return _d
1364 1365
1365 1366 # force locked state through a flag
1366 1367 else:
1367 1368 locked = str2bool(locked)
1368 1369 lock_reason = Repository.LOCK_API
1369 1370 try:
1370 1371 if locked:
1371 1372 lock_time = time.time()
1372 1373 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1373 1374 else:
1374 1375 lock_time = None
1375 1376 Repository.unlock(repo)
1376 1377 _d = {
1377 1378 'repo': repo.repo_name,
1378 1379 'locked': locked,
1379 1380 'locked_since': lock_time,
1380 1381 'locked_by': user.username,
1381 1382 'lock_reason': lock_reason,
1382 1383 'lock_state_changed': True,
1383 1384 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1384 1385 % (user.username, repo.repo_name, locked))
1385 1386 }
1386 1387 return _d
1387 1388 except Exception:
1388 1389 log.exception(
1389 1390 "Exception occurred while trying to lock repository")
1390 1391 raise JSONRPCError(
1391 1392 'Error occurred locking repository `%s`' % repo.repo_name
1392 1393 )
1393 1394
1394 1395
1395 1396 @jsonrpc_method()
1396 1397 def comment_commit(
1397 1398 request, apiuser, repoid, commit_id, message, status=Optional(None),
1398 1399 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1399 1400 resolves_comment_id=Optional(None),
1400 1401 userid=Optional(OAttr('apiuser'))):
1401 1402 """
1402 1403 Set a commit comment, and optionally change the status of the commit.
1403 1404
1404 1405 :param apiuser: This is filled automatically from the |authtoken|.
1405 1406 :type apiuser: AuthUser
1406 1407 :param repoid: Set the repository name or repository ID.
1407 1408 :type repoid: str or int
1408 1409 :param commit_id: Specify the commit_id for which to set a comment.
1409 1410 :type commit_id: str
1410 1411 :param message: The comment text.
1411 1412 :type message: str
1412 1413 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1413 1414 'approved', 'rejected', 'under_review'
1414 1415 :type status: str
1415 1416 :param comment_type: Comment type, one of: 'note', 'todo'
1416 1417 :type comment_type: Optional(str), default: 'note'
1417 1418 :param userid: Set the user name of the comment creator.
1418 1419 :type userid: Optional(str or int)
1419 1420
1420 1421 Example error output:
1421 1422
1422 1423 .. code-block:: bash
1423 1424
1424 1425 {
1425 1426 "id" : <id_given_in_input>,
1426 1427 "result" : {
1427 1428 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1428 1429 "status_change": null or <status>,
1429 1430 "success": true
1430 1431 },
1431 1432 "error" : null
1432 1433 }
1433 1434
1434 1435 """
1435 1436 repo = get_repo_or_error(repoid)
1436 1437 if not has_superadmin_permission(apiuser):
1437 1438 _perms = ('repository.read', 'repository.write', 'repository.admin')
1438 1439 validate_repo_permissions(apiuser, repoid, repo, _perms)
1439 1440
1440 1441 try:
1441 1442 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1442 1443 except Exception as e:
1443 1444 log.exception('Failed to fetch commit')
1444 1445 raise JSONRPCError(e.message)
1445 1446
1446 1447 if isinstance(userid, Optional):
1447 1448 userid = apiuser.user_id
1448 1449
1449 1450 user = get_user_or_error(userid)
1450 1451 status = Optional.extract(status)
1451 1452 comment_type = Optional.extract(comment_type)
1452 1453 resolves_comment_id = Optional.extract(resolves_comment_id)
1453 1454
1454 1455 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1455 1456 if status and status not in allowed_statuses:
1456 1457 raise JSONRPCError('Bad status, must be on '
1457 1458 'of %s got %s' % (allowed_statuses, status,))
1458 1459
1459 1460 if resolves_comment_id:
1460 1461 comment = ChangesetComment.get(resolves_comment_id)
1461 1462 if not comment:
1462 1463 raise JSONRPCError(
1463 1464 'Invalid resolves_comment_id `%s` for this commit.'
1464 1465 % resolves_comment_id)
1465 1466 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1466 1467 raise JSONRPCError(
1467 1468 'Comment `%s` is wrong type for setting status to resolved.'
1468 1469 % resolves_comment_id)
1469 1470
1470 1471 try:
1471 1472 rc_config = SettingsModel().get_all_settings()
1472 1473 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1473 1474 status_change_label = ChangesetStatus.get_status_lbl(status)
1474 1475 comment = CommentsModel().create(
1475 1476 message, repo, user, commit_id=commit_id,
1476 1477 status_change=status_change_label,
1477 1478 status_change_type=status,
1478 1479 renderer=renderer,
1479 1480 comment_type=comment_type,
1480 1481 resolves_comment_id=resolves_comment_id
1481 1482 )
1482 1483 if status:
1483 1484 # also do a status change
1484 1485 try:
1485 1486 ChangesetStatusModel().set_status(
1486 1487 repo, status, user, comment, revision=commit_id,
1487 1488 dont_allow_on_closed_pull_request=True
1488 1489 )
1489 1490 except StatusChangeOnClosedPullRequestError:
1490 1491 log.exception(
1491 1492 "Exception occurred while trying to change repo commit status")
1492 1493 msg = ('Changing status on a changeset associated with '
1493 1494 'a closed pull request is not allowed')
1494 1495 raise JSONRPCError(msg)
1495 1496
1496 1497 Session().commit()
1497 1498 return {
1498 1499 'msg': (
1499 1500 'Commented on commit `%s` for repository `%s`' % (
1500 1501 comment.revision, repo.repo_name)),
1501 1502 'status_change': status,
1502 1503 'success': True,
1503 1504 }
1504 1505 except JSONRPCError:
1505 1506 # catch any inside errors, and re-raise them to prevent from
1506 1507 # below global catch to silence them
1507 1508 raise
1508 1509 except Exception:
1509 1510 log.exception("Exception occurred while trying to comment on commit")
1510 1511 raise JSONRPCError(
1511 1512 'failed to set comment on repository `%s`' % (repo.repo_name,)
1512 1513 )
1513 1514
1514 1515
1515 1516 @jsonrpc_method()
1516 1517 def grant_user_permission(request, apiuser, repoid, userid, perm):
1517 1518 """
1518 1519 Grant permissions for the specified user on the given repository,
1519 1520 or update existing permissions if found.
1520 1521
1521 1522 This command can only be run using an |authtoken| with admin
1522 1523 permissions on the |repo|.
1523 1524
1524 1525 :param apiuser: This is filled automatically from the |authtoken|.
1525 1526 :type apiuser: AuthUser
1526 1527 :param repoid: Set the repository name or repository ID.
1527 1528 :type repoid: str or int
1528 1529 :param userid: Set the user name.
1529 1530 :type userid: str
1530 1531 :param perm: Set the user permissions, using the following format
1531 1532 ``(repository.(none|read|write|admin))``
1532 1533 :type perm: str
1533 1534
1534 1535 Example output:
1535 1536
1536 1537 .. code-block:: bash
1537 1538
1538 1539 id : <id_given_in_input>
1539 1540 result: {
1540 1541 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1541 1542 "success": true
1542 1543 }
1543 1544 error: null
1544 1545 """
1545 1546
1546 1547 repo = get_repo_or_error(repoid)
1547 1548 user = get_user_or_error(userid)
1548 1549 perm = get_perm_or_error(perm)
1549 1550 if not has_superadmin_permission(apiuser):
1550 1551 _perms = ('repository.admin',)
1551 1552 validate_repo_permissions(apiuser, repoid, repo, _perms)
1552 1553
1553 1554 try:
1554 1555
1555 1556 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1556 1557
1557 1558 Session().commit()
1558 1559 return {
1559 1560 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1560 1561 perm.permission_name, user.username, repo.repo_name
1561 1562 ),
1562 1563 'success': True
1563 1564 }
1564 1565 except Exception:
1565 1566 log.exception(
1566 1567 "Exception occurred while trying edit permissions for repo")
1567 1568 raise JSONRPCError(
1568 1569 'failed to edit permission for user: `%s` in repo: `%s`' % (
1569 1570 userid, repoid
1570 1571 )
1571 1572 )
1572 1573
1573 1574
1574 1575 @jsonrpc_method()
1575 1576 def revoke_user_permission(request, apiuser, repoid, userid):
1576 1577 """
1577 1578 Revoke permission for a user on the specified repository.
1578 1579
1579 1580 This command can only be run using an |authtoken| with admin
1580 1581 permissions on the |repo|.
1581 1582
1582 1583 :param apiuser: This is filled automatically from the |authtoken|.
1583 1584 :type apiuser: AuthUser
1584 1585 :param repoid: Set the repository name or repository ID.
1585 1586 :type repoid: str or int
1586 1587 :param userid: Set the user name of revoked user.
1587 1588 :type userid: str or int
1588 1589
1589 1590 Example error output:
1590 1591
1591 1592 .. code-block:: bash
1592 1593
1593 1594 id : <id_given_in_input>
1594 1595 result: {
1595 1596 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1596 1597 "success": true
1597 1598 }
1598 1599 error: null
1599 1600 """
1600 1601
1601 1602 repo = get_repo_or_error(repoid)
1602 1603 user = get_user_or_error(userid)
1603 1604 if not has_superadmin_permission(apiuser):
1604 1605 _perms = ('repository.admin',)
1605 1606 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606 1607
1607 1608 try:
1608 1609 RepoModel().revoke_user_permission(repo=repo, user=user)
1609 1610 Session().commit()
1610 1611 return {
1611 1612 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1612 1613 user.username, repo.repo_name
1613 1614 ),
1614 1615 'success': True
1615 1616 }
1616 1617 except Exception:
1617 1618 log.exception(
1618 1619 "Exception occurred while trying revoke permissions to repo")
1619 1620 raise JSONRPCError(
1620 1621 'failed to edit permission for user: `%s` in repo: `%s`' % (
1621 1622 userid, repoid
1622 1623 )
1623 1624 )
1624 1625
1625 1626
1626 1627 @jsonrpc_method()
1627 1628 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1628 1629 """
1629 1630 Grant permission for a user group on the specified repository,
1630 1631 or update existing permissions.
1631 1632
1632 1633 This command can only be run using an |authtoken| with admin
1633 1634 permissions on the |repo|.
1634 1635
1635 1636 :param apiuser: This is filled automatically from the |authtoken|.
1636 1637 :type apiuser: AuthUser
1637 1638 :param repoid: Set the repository name or repository ID.
1638 1639 :type repoid: str or int
1639 1640 :param usergroupid: Specify the ID of the user group.
1640 1641 :type usergroupid: str or int
1641 1642 :param perm: Set the user group permissions using the following
1642 1643 format: (repository.(none|read|write|admin))
1643 1644 :type perm: str
1644 1645
1645 1646 Example output:
1646 1647
1647 1648 .. code-block:: bash
1648 1649
1649 1650 id : <id_given_in_input>
1650 1651 result : {
1651 1652 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1652 1653 "success": true
1653 1654
1654 1655 }
1655 1656 error : null
1656 1657
1657 1658 Example error output:
1658 1659
1659 1660 .. code-block:: bash
1660 1661
1661 1662 id : <id_given_in_input>
1662 1663 result : null
1663 1664 error : {
1664 1665 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1665 1666 }
1666 1667
1667 1668 """
1668 1669
1669 1670 repo = get_repo_or_error(repoid)
1670 1671 perm = get_perm_or_error(perm)
1671 1672 if not has_superadmin_permission(apiuser):
1672 1673 _perms = ('repository.admin',)
1673 1674 validate_repo_permissions(apiuser, repoid, repo, _perms)
1674 1675
1675 1676 user_group = get_user_group_or_error(usergroupid)
1676 1677 if not has_superadmin_permission(apiuser):
1677 1678 # check if we have at least read permission for this user group !
1678 1679 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1679 1680 if not HasUserGroupPermissionAnyApi(*_perms)(
1680 1681 user=apiuser, user_group_name=user_group.users_group_name):
1681 1682 raise JSONRPCError(
1682 1683 'user group `%s` does not exist' % (usergroupid,))
1683 1684
1684 1685 try:
1685 1686 RepoModel().grant_user_group_permission(
1686 1687 repo=repo, group_name=user_group, perm=perm)
1687 1688
1688 1689 Session().commit()
1689 1690 return {
1690 1691 'msg': 'Granted perm: `%s` for user group: `%s` in '
1691 1692 'repo: `%s`' % (
1692 1693 perm.permission_name, user_group.users_group_name,
1693 1694 repo.repo_name
1694 1695 ),
1695 1696 'success': True
1696 1697 }
1697 1698 except Exception:
1698 1699 log.exception(
1699 1700 "Exception occurred while trying change permission on repo")
1700 1701 raise JSONRPCError(
1701 1702 'failed to edit permission for user group: `%s` in '
1702 1703 'repo: `%s`' % (
1703 1704 usergroupid, repo.repo_name
1704 1705 )
1705 1706 )
1706 1707
1707 1708
1708 1709 @jsonrpc_method()
1709 1710 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1710 1711 """
1711 1712 Revoke the permissions of a user group on a given repository.
1712 1713
1713 1714 This command can only be run using an |authtoken| with admin
1714 1715 permissions on the |repo|.
1715 1716
1716 1717 :param apiuser: This is filled automatically from the |authtoken|.
1717 1718 :type apiuser: AuthUser
1718 1719 :param repoid: Set the repository name or repository ID.
1719 1720 :type repoid: str or int
1720 1721 :param usergroupid: Specify the user group ID.
1721 1722 :type usergroupid: str or int
1722 1723
1723 1724 Example output:
1724 1725
1725 1726 .. code-block:: bash
1726 1727
1727 1728 id : <id_given_in_input>
1728 1729 result: {
1729 1730 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1730 1731 "success": true
1731 1732 }
1732 1733 error: null
1733 1734 """
1734 1735
1735 1736 repo = get_repo_or_error(repoid)
1736 1737 if not has_superadmin_permission(apiuser):
1737 1738 _perms = ('repository.admin',)
1738 1739 validate_repo_permissions(apiuser, repoid, repo, _perms)
1739 1740
1740 1741 user_group = get_user_group_or_error(usergroupid)
1741 1742 if not has_superadmin_permission(apiuser):
1742 1743 # check if we have at least read permission for this user group !
1743 1744 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1744 1745 if not HasUserGroupPermissionAnyApi(*_perms)(
1745 1746 user=apiuser, user_group_name=user_group.users_group_name):
1746 1747 raise JSONRPCError(
1747 1748 'user group `%s` does not exist' % (usergroupid,))
1748 1749
1749 1750 try:
1750 1751 RepoModel().revoke_user_group_permission(
1751 1752 repo=repo, group_name=user_group)
1752 1753
1753 1754 Session().commit()
1754 1755 return {
1755 1756 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1756 1757 user_group.users_group_name, repo.repo_name
1757 1758 ),
1758 1759 'success': True
1759 1760 }
1760 1761 except Exception:
1761 1762 log.exception("Exception occurred while trying revoke "
1762 1763 "user group permission on repo")
1763 1764 raise JSONRPCError(
1764 1765 'failed to edit permission for user group: `%s` in '
1765 1766 'repo: `%s`' % (
1766 1767 user_group.users_group_name, repo.repo_name
1767 1768 )
1768 1769 )
1769 1770
1770 1771
1771 1772 @jsonrpc_method()
1772 1773 def pull(request, apiuser, repoid):
1773 1774 """
1774 1775 Triggers a pull on the given repository from a remote location. You
1775 1776 can use this to keep remote repositories up-to-date.
1776 1777
1777 1778 This command can only be run using an |authtoken| with admin
1778 1779 rights to the specified repository. For more information,
1779 1780 see :ref:`config-token-ref`.
1780 1781
1781 1782 This command takes the following options:
1782 1783
1783 1784 :param apiuser: This is filled automatically from the |authtoken|.
1784 1785 :type apiuser: AuthUser
1785 1786 :param repoid: The repository name or repository ID.
1786 1787 :type repoid: str or int
1787 1788
1788 1789 Example output:
1789 1790
1790 1791 .. code-block:: bash
1791 1792
1792 1793 id : <id_given_in_input>
1793 1794 result : {
1794 1795 "msg": "Pulled from `<repository name>`"
1795 1796 "repository": "<repository name>"
1796 1797 }
1797 1798 error : null
1798 1799
1799 1800 Example error output:
1800 1801
1801 1802 .. code-block:: bash
1802 1803
1803 1804 id : <id_given_in_input>
1804 1805 result : null
1805 1806 error : {
1806 1807 "Unable to pull changes from `<reponame>`"
1807 1808 }
1808 1809
1809 1810 """
1810 1811
1811 1812 repo = get_repo_or_error(repoid)
1812 1813 if not has_superadmin_permission(apiuser):
1813 1814 _perms = ('repository.admin',)
1814 1815 validate_repo_permissions(apiuser, repoid, repo, _perms)
1815 1816
1816 1817 try:
1817 1818 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1818 1819 return {
1819 1820 'msg': 'Pulled from `%s`' % repo.repo_name,
1820 1821 'repository': repo.repo_name
1821 1822 }
1822 1823 except Exception:
1823 1824 log.exception("Exception occurred while trying to "
1824 1825 "pull changes from remote location")
1825 1826 raise JSONRPCError(
1826 1827 'Unable to pull changes from `%s`' % repo.repo_name
1827 1828 )
1828 1829
1829 1830
1830 1831 @jsonrpc_method()
1831 1832 def strip(request, apiuser, repoid, revision, branch):
1832 1833 """
1833 1834 Strips the given revision from the specified repository.
1834 1835
1835 1836 * This will remove the revision and all of its decendants.
1836 1837
1837 1838 This command can only be run using an |authtoken| with admin rights to
1838 1839 the specified repository.
1839 1840
1840 1841 This command takes the following options:
1841 1842
1842 1843 :param apiuser: This is filled automatically from the |authtoken|.
1843 1844 :type apiuser: AuthUser
1844 1845 :param repoid: The repository name or repository ID.
1845 1846 :type repoid: str or int
1846 1847 :param revision: The revision you wish to strip.
1847 1848 :type revision: str
1848 1849 :param branch: The branch from which to strip the revision.
1849 1850 :type branch: str
1850 1851
1851 1852 Example output:
1852 1853
1853 1854 .. code-block:: bash
1854 1855
1855 1856 id : <id_given_in_input>
1856 1857 result : {
1857 1858 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1858 1859 "repository": "<repository name>"
1859 1860 }
1860 1861 error : null
1861 1862
1862 1863 Example error output:
1863 1864
1864 1865 .. code-block:: bash
1865 1866
1866 1867 id : <id_given_in_input>
1867 1868 result : null
1868 1869 error : {
1869 1870 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1870 1871 }
1871 1872
1872 1873 """
1873 1874
1874 1875 repo = get_repo_or_error(repoid)
1875 1876 if not has_superadmin_permission(apiuser):
1876 1877 _perms = ('repository.admin',)
1877 1878 validate_repo_permissions(apiuser, repoid, repo, _perms)
1878 1879
1879 1880 try:
1880 1881 ScmModel().strip(repo, revision, branch)
1881 1882 audit_logger.store_api(
1882 1883 'repo.commit.strip', action_data={'commit_id': revision},
1883 1884 repo=repo,
1884 1885 user=apiuser, commit=True)
1885 1886
1886 1887 return {
1887 1888 'msg': 'Stripped commit %s from repo `%s`' % (
1888 1889 revision, repo.repo_name),
1889 1890 'repository': repo.repo_name
1890 1891 }
1891 1892 except Exception:
1892 1893 log.exception("Exception while trying to strip")
1893 1894 raise JSONRPCError(
1894 1895 'Unable to strip commit %s from repo `%s`' % (
1895 1896 revision, repo.repo_name)
1896 1897 )
1897 1898
1898 1899
1899 1900 @jsonrpc_method()
1900 1901 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1901 1902 """
1902 1903 Returns all settings for a repository. If key is given it only returns the
1903 1904 setting identified by the key or null.
1904 1905
1905 1906 :param apiuser: This is filled automatically from the |authtoken|.
1906 1907 :type apiuser: AuthUser
1907 1908 :param repoid: The repository name or repository id.
1908 1909 :type repoid: str or int
1909 1910 :param key: Key of the setting to return.
1910 1911 :type: key: Optional(str)
1911 1912
1912 1913 Example output:
1913 1914
1914 1915 .. code-block:: bash
1915 1916
1916 1917 {
1917 1918 "error": null,
1918 1919 "id": 237,
1919 1920 "result": {
1920 1921 "extensions_largefiles": true,
1921 1922 "extensions_evolve": true,
1922 1923 "hooks_changegroup_push_logger": true,
1923 1924 "hooks_changegroup_repo_size": false,
1924 1925 "hooks_outgoing_pull_logger": true,
1925 1926 "phases_publish": "True",
1926 1927 "rhodecode_hg_use_rebase_for_merging": true,
1927 1928 "rhodecode_pr_merge_enabled": true,
1928 1929 "rhodecode_use_outdated_comments": true
1929 1930 }
1930 1931 }
1931 1932 """
1932 1933
1933 1934 # Restrict access to this api method to admins only.
1934 1935 if not has_superadmin_permission(apiuser):
1935 1936 raise JSONRPCForbidden()
1936 1937
1937 1938 try:
1938 1939 repo = get_repo_or_error(repoid)
1939 1940 settings_model = VcsSettingsModel(repo=repo)
1940 1941 settings = settings_model.get_global_settings()
1941 1942 settings.update(settings_model.get_repo_settings())
1942 1943
1943 1944 # If only a single setting is requested fetch it from all settings.
1944 1945 key = Optional.extract(key)
1945 1946 if key is not None:
1946 1947 settings = settings.get(key, None)
1947 1948 except Exception:
1948 1949 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1949 1950 log.exception(msg)
1950 1951 raise JSONRPCError(msg)
1951 1952
1952 1953 return settings
1953 1954
1954 1955
1955 1956 @jsonrpc_method()
1956 1957 def set_repo_settings(request, apiuser, repoid, settings):
1957 1958 """
1958 1959 Update repository settings. Returns true on success.
1959 1960
1960 1961 :param apiuser: This is filled automatically from the |authtoken|.
1961 1962 :type apiuser: AuthUser
1962 1963 :param repoid: The repository name or repository id.
1963 1964 :type repoid: str or int
1964 1965 :param settings: The new settings for the repository.
1965 1966 :type: settings: dict
1966 1967
1967 1968 Example output:
1968 1969
1969 1970 .. code-block:: bash
1970 1971
1971 1972 {
1972 1973 "error": null,
1973 1974 "id": 237,
1974 1975 "result": true
1975 1976 }
1976 1977 """
1977 1978 # Restrict access to this api method to admins only.
1978 1979 if not has_superadmin_permission(apiuser):
1979 1980 raise JSONRPCForbidden()
1980 1981
1981 1982 if type(settings) is not dict:
1982 1983 raise JSONRPCError('Settings have to be a JSON Object.')
1983 1984
1984 1985 try:
1985 1986 settings_model = VcsSettingsModel(repo=repoid)
1986 1987
1987 1988 # Merge global, repo and incoming settings.
1988 1989 new_settings = settings_model.get_global_settings()
1989 1990 new_settings.update(settings_model.get_repo_settings())
1990 1991 new_settings.update(settings)
1991 1992
1992 1993 # Update the settings.
1993 1994 inherit_global_settings = new_settings.get(
1994 1995 'inherit_global_settings', False)
1995 1996 settings_model.create_or_update_repo_settings(
1996 1997 new_settings, inherit_global_settings=inherit_global_settings)
1997 1998 Session().commit()
1998 1999 except Exception:
1999 2000 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2000 2001 log.exception(msg)
2001 2002 raise JSONRPCError(msg)
2002 2003
2003 2004 # Indicate success.
2004 2005 return True
2005 2006
2006 2007
2007 2008 @jsonrpc_method()
2008 2009 def maintenance(request, apiuser, repoid):
2009 2010 """
2010 2011 Triggers a maintenance on the given repository.
2011 2012
2012 2013 This command can only be run using an |authtoken| with admin
2013 2014 rights to the specified repository. For more information,
2014 2015 see :ref:`config-token-ref`.
2015 2016
2016 2017 This command takes the following options:
2017 2018
2018 2019 :param apiuser: This is filled automatically from the |authtoken|.
2019 2020 :type apiuser: AuthUser
2020 2021 :param repoid: The repository name or repository ID.
2021 2022 :type repoid: str or int
2022 2023
2023 2024 Example output:
2024 2025
2025 2026 .. code-block:: bash
2026 2027
2027 2028 id : <id_given_in_input>
2028 2029 result : {
2029 2030 "msg": "executed maintenance command",
2030 2031 "executed_actions": [
2031 2032 <action_message>, <action_message2>...
2032 2033 ],
2033 2034 "repository": "<repository name>"
2034 2035 }
2035 2036 error : null
2036 2037
2037 2038 Example error output:
2038 2039
2039 2040 .. code-block:: bash
2040 2041
2041 2042 id : <id_given_in_input>
2042 2043 result : null
2043 2044 error : {
2044 2045 "Unable to execute maintenance on `<reponame>`"
2045 2046 }
2046 2047
2047 2048 """
2048 2049
2049 2050 repo = get_repo_or_error(repoid)
2050 2051 if not has_superadmin_permission(apiuser):
2051 2052 _perms = ('repository.admin',)
2052 2053 validate_repo_permissions(apiuser, repoid, repo, _perms)
2053 2054
2054 2055 try:
2055 2056 maintenance = repo_maintenance.RepoMaintenance()
2056 2057 executed_actions = maintenance.execute(repo)
2057 2058
2058 2059 return {
2059 2060 'msg': 'executed maintenance command',
2060 2061 'executed_actions': executed_actions,
2061 2062 'repository': repo.repo_name
2062 2063 }
2063 2064 except Exception:
2064 2065 log.exception("Exception occurred while trying to run maintenance")
2065 2066 raise JSONRPCError(
2066 2067 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,201 +1,201 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests import assert_session_flash
24 24 from rhodecode.tests.utils import AssertResponse
25 25 from rhodecode.model.db import Session
26 26 from rhodecode.model.settings import SettingsModel
27 27
28 28
29 29 def assert_auth_settings_updated(response):
30 30 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 31 assert_session_flash(response, 'Auth settings updated successfully')
32 32
33 33
34 34 @pytest.mark.usefixtures("autologin_user", "app")
35 class TestAuthSettingsController(object):
35 class TestAuthSettingsView(object):
36 36
37 37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 38 verify_response=False):
39 39 test_url = '/_admin/auth'
40 40 params = {
41 41 'auth_plugins': plugins_list,
42 42 'csrf_token': csrf_token,
43 43 }
44 44 if override:
45 45 params.update(override)
46 46 _enabled_plugins = []
47 47 for plugin in plugins_list.split(','):
48 48 plugin_name = plugin.partition('#')[-1]
49 49 enabled_plugin = '%s_enabled' % plugin_name
50 50 cache_ttl = '%s_cache_ttl' % plugin_name
51 51
52 52 # default params that are needed for each plugin,
53 53 # `enabled` and `cache_ttl`
54 54 params.update({
55 55 enabled_plugin: True,
56 56 cache_ttl: 0
57 57 })
58 58 _enabled_plugins.append(enabled_plugin)
59 59
60 60 # we need to clean any enabled plugin before, since they require
61 61 # form params to be present
62 62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 63 db_plugin.app_settings_value = \
64 64 'egg:rhodecode-enterprise-ce#rhodecode'
65 65 Session().add(db_plugin)
66 66 Session().commit()
67 67 for _plugin in _enabled_plugins:
68 68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 69 if db_plugin:
70 70 Session().delete(db_plugin)
71 71 Session().commit()
72 72
73 73 response = self.app.post(url=test_url, params=params)
74 74
75 75 if verify_response:
76 76 assert_auth_settings_updated(response)
77 77 return params
78 78
79 79 def _post_ldap_settings(self, params, override=None, force=False):
80 80
81 81 params.update({
82 82 'filter': 'user',
83 83 'user_member_of': '',
84 84 'user_search_base': '',
85 85 'user_search_filter': 'test_filter',
86 86
87 87 'host': 'dc.example.com',
88 88 'port': '999',
89 89 'tls_kind': 'PLAIN',
90 90 'tls_reqcert': 'NEVER',
91 91
92 92 'dn_user': 'test_user',
93 93 'dn_pass': 'test_pass',
94 94 'base_dn': 'test_base_dn',
95 95 'search_scope': 'BASE',
96 96 'attr_login': 'test_attr_login',
97 97 'attr_firstname': 'ima',
98 98 'attr_lastname': 'tester',
99 99 'attr_email': 'test@example.com',
100 100 'cache_ttl': '0',
101 101 })
102 102 if force:
103 103 params = {}
104 104 params.update(override or {})
105 105
106 106 test_url = '/_admin/auth/ldap/'
107 107
108 108 response = self.app.post(url=test_url, params=params)
109 109 return response
110 110
111 111 def test_index(self):
112 112 response = self.app.get('/_admin/auth')
113 113 response.mustcontain('Authentication Plugins')
114 114
115 115 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 116 ('egg:rhodecode-enterprise-ce#headers', None),
117 117 ('egg:rhodecode-enterprise-ce#crowd', None),
118 118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 119 ('egg:rhodecode-enterprise-ce#ldap', None),
120 120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 121 ])
122 122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 123 # TODO: johbo: "pam" is currently not available on darwin,
124 124 # although the docs state that it should work on darwin.
125 125 if needs_import:
126 126 pytest.importorskip(needs_import)
127 127
128 128 self._enable_plugins(
129 129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 130 csrf_token, verify_response=True)
131 131
132 132 self._enable_plugins(
133 133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 134 verify_response=True)
135 135
136 136 def test_ldap_save_settings(self, csrf_token):
137 137 params = self._enable_plugins(
138 138 'egg:rhodecode-enterprise-ce#rhodecode,'
139 139 'egg:rhodecode-enterprise-ce#ldap',
140 140 csrf_token)
141 141 response = self._post_ldap_settings(params)
142 142 assert_auth_settings_updated(response)
143 143
144 144 new_settings = SettingsModel().get_auth_settings()
145 145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 146 'fail db write compare'
147 147
148 148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 149 params = self._enable_plugins(
150 150 'egg:rhodecode-enterprise-ce#rhodecode,'
151 151 'egg:rhodecode-enterprise-ce#ldap',
152 152 csrf_token)
153 153 invalid_port_value = 'invalid-port-number'
154 154 response = self._post_ldap_settings(params, override={
155 155 'port': invalid_port_value,
156 156 })
157 157 assertr = AssertResponse(response)
158 158 assertr.element_contains(
159 159 '.form .field #port ~ .error-message',
160 160 invalid_port_value)
161 161
162 162 def test_ldap_error_form(self, csrf_token):
163 163 params = self._enable_plugins(
164 164 'egg:rhodecode-enterprise-ce#rhodecode,'
165 165 'egg:rhodecode-enterprise-ce#ldap',
166 166 csrf_token)
167 167 response = self._post_ldap_settings(params, override={
168 168 'attr_login': '',
169 169 })
170 170 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 171 """ attribute of the CN must be specified""")
172 172
173 173 def test_post_ldap_group_settings(self, csrf_token):
174 174 params = self._enable_plugins(
175 175 'egg:rhodecode-enterprise-ce#rhodecode,'
176 176 'egg:rhodecode-enterprise-ce#ldap',
177 177 csrf_token)
178 178
179 179 response = self._post_ldap_settings(params, override={
180 180 'host': 'dc-legacy.example.com',
181 181 'port': '999',
182 182 'tls_kind': 'PLAIN',
183 183 'tls_reqcert': 'NEVER',
184 184 'dn_user': 'test_user',
185 185 'dn_pass': 'test_pass',
186 186 'base_dn': 'test_base_dn',
187 187 'filter': 'test_filter',
188 188 'search_scope': 'BASE',
189 189 'attr_login': 'test_attr_login',
190 190 'attr_firstname': 'ima',
191 191 'attr_lastname': 'tester',
192 192 'attr_email': 'test@example.com',
193 193 'cache_ttl': '60',
194 194 'csrf_token': csrf_token,
195 195 }
196 196 )
197 197 assert_auth_settings_updated(response)
198 198
199 199 new_settings = SettingsModel().get_auth_settings()
200 200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 201 'fail db write compare'
@@ -1,781 +1,781 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22 from sqlalchemy.orm.exc import NoResultFound
23 23
24 24 from rhodecode.lib import auth
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.user import UserModel
29 29
30 30 from rhodecode.tests import (
31 31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 32 from rhodecode.tests.fixture import Fixture
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42 'users':
43 43 ADMIN_PREFIX + '/users',
44 44 'users_data':
45 45 ADMIN_PREFIX + '/users_data',
46 46 'users_create':
47 47 ADMIN_PREFIX + '/users/create',
48 48 'users_new':
49 49 ADMIN_PREFIX + '/users/new',
50 50 'user_edit':
51 51 ADMIN_PREFIX + '/users/{user_id}/edit',
52 52 'user_edit_advanced':
53 53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 54 'user_edit_global_perms':
55 55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 56 'user_edit_global_perms_update':
57 57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 58 'user_update':
59 59 ADMIN_PREFIX + '/users/{user_id}/update',
60 60 'user_delete':
61 61 ADMIN_PREFIX + '/users/{user_id}/delete',
62 62 'user_force_password_reset':
63 63 ADMIN_PREFIX + '/users/{user_id}/password_reset',
64 64 'user_create_personal_repo_group':
65 65 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
66 66
67 67 'edit_user_auth_tokens':
68 68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
69 69 'edit_user_auth_tokens_add':
70 70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
71 71 'edit_user_auth_tokens_delete':
72 72 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
73 73
74 74 'edit_user_emails':
75 75 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
76 76 'edit_user_emails_add':
77 77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
78 78 'edit_user_emails_delete':
79 79 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
80 80
81 81 'edit_user_ips':
82 82 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
83 83 'edit_user_ips_add':
84 84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
85 85 'edit_user_ips_delete':
86 86 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
87 87
88 88 'edit_user_perms_summary':
89 89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
90 90 'edit_user_perms_summary_json':
91 91 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
92 92
93 93 'edit_user_audit_logs':
94 94 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
95 95
96 96 }[name].format(**kwargs)
97 97
98 98 if params:
99 99 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
100 100 return base_url
101 101
102 102
103 103 class TestAdminUsersView(TestController):
104 104
105 105 def test_show_users(self):
106 106 self.log_user()
107 107 self.app.get(route_path('users'))
108 108
109 109 def test_show_users_data(self, xhr_header):
110 110 self.log_user()
111 111 response = self.app.get(route_path(
112 112 'users_data'), extra_environ=xhr_header)
113 113
114 114 all_users = User.query().filter(
115 115 User.username != User.DEFAULT_USER).count()
116 116 assert response.json['recordsTotal'] == all_users
117 117
118 118 def test_show_users_data_filtered(self, xhr_header):
119 119 self.log_user()
120 120 response = self.app.get(route_path(
121 121 'users_data', params={'search[value]': 'empty_search'}),
122 122 extra_environ=xhr_header)
123 123
124 124 all_users = User.query().filter(
125 125 User.username != User.DEFAULT_USER).count()
126 126 assert response.json['recordsTotal'] == all_users
127 127 assert response.json['recordsFiltered'] == 0
128 128
129 129 def test_auth_tokens_default_user(self):
130 130 self.log_user()
131 131 user = User.get_default_user()
132 132 response = self.app.get(
133 133 route_path('edit_user_auth_tokens', user_id=user.user_id),
134 134 status=302)
135 135
136 136 def test_auth_tokens(self):
137 137 self.log_user()
138 138
139 139 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
140 140 response = self.app.get(
141 141 route_path('edit_user_auth_tokens', user_id=user.user_id))
142 142 for token in user.auth_tokens:
143 143 response.mustcontain(token)
144 144 response.mustcontain('never')
145 145
146 146 @pytest.mark.parametrize("desc, lifetime", [
147 147 ('forever', -1),
148 148 ('5mins', 60*5),
149 149 ('30days', 60*60*24*30),
150 150 ])
151 151 def test_add_auth_token(self, desc, lifetime, user_util):
152 152 self.log_user()
153 153 user = user_util.create_user()
154 154 user_id = user.user_id
155 155
156 156 response = self.app.post(
157 157 route_path('edit_user_auth_tokens_add', user_id=user_id),
158 158 {'description': desc, 'lifetime': lifetime,
159 159 'csrf_token': self.csrf_token})
160 160 assert_session_flash(response, 'Auth token successfully created')
161 161
162 162 response = response.follow()
163 163 user = User.get(user_id)
164 164 for auth_token in user.auth_tokens:
165 165 response.mustcontain(auth_token)
166 166
167 167 def test_delete_auth_token(self, user_util):
168 168 self.log_user()
169 169 user = user_util.create_user()
170 170 user_id = user.user_id
171 171 keys = user.auth_tokens
172 172 assert 2 == len(keys)
173 173
174 174 response = self.app.post(
175 175 route_path('edit_user_auth_tokens_add', user_id=user_id),
176 176 {'description': 'desc', 'lifetime': -1,
177 177 'csrf_token': self.csrf_token})
178 178 assert_session_flash(response, 'Auth token successfully created')
179 179 response.follow()
180 180
181 181 # now delete our key
182 182 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
183 183 assert 3 == len(keys)
184 184
185 185 response = self.app.post(
186 186 route_path('edit_user_auth_tokens_delete', user_id=user_id),
187 187 {'del_auth_token': keys[0].user_api_key_id,
188 188 'csrf_token': self.csrf_token})
189 189
190 190 assert_session_flash(response, 'Auth token successfully deleted')
191 191 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
192 192 assert 2 == len(keys)
193 193
194 194 def test_ips(self):
195 195 self.log_user()
196 196 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
197 197 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
198 198 response.mustcontain('All IP addresses are allowed')
199 199
200 200 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
201 201 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
202 202 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
203 203 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
204 204 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
205 205 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
206 206 ('127_bad_ip', 'foobar', 'foobar', True),
207 207 ])
208 208 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
209 209 self.log_user()
210 210 user = user_util.create_user(username=test_name)
211 211 user_id = user.user_id
212 212
213 213 response = self.app.post(
214 214 route_path('edit_user_ips_add', user_id=user_id),
215 215 params={'new_ip': ip, 'csrf_token': self.csrf_token})
216 216
217 217 if failure:
218 218 assert_session_flash(
219 219 response, 'Please enter a valid IPv4 or IpV6 address')
220 220 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
221 221
222 222 response.mustcontain(no=[ip])
223 223 response.mustcontain(no=[ip_range])
224 224
225 225 else:
226 226 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
227 227 response.mustcontain(ip)
228 228 response.mustcontain(ip_range)
229 229
230 230 def test_ips_delete(self, user_util):
231 231 self.log_user()
232 232 user = user_util.create_user()
233 233 user_id = user.user_id
234 234 ip = '127.0.0.1/32'
235 235 ip_range = '127.0.0.1 - 127.0.0.1'
236 236 new_ip = UserModel().add_extra_ip(user_id, ip)
237 237 Session().commit()
238 238 new_ip_id = new_ip.ip_id
239 239
240 240 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
241 241 response.mustcontain(ip)
242 242 response.mustcontain(ip_range)
243 243
244 244 self.app.post(
245 245 route_path('edit_user_ips_delete', user_id=user_id),
246 246 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
247 247
248 248 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
249 249 response.mustcontain('All IP addresses are allowed')
250 250 response.mustcontain(no=[ip])
251 251 response.mustcontain(no=[ip_range])
252 252
253 253 def test_emails(self):
254 254 self.log_user()
255 255 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
256 256 response = self.app.get(
257 257 route_path('edit_user_emails', user_id=user.user_id))
258 258 response.mustcontain('No additional emails specified')
259 259
260 260 def test_emails_add(self, user_util):
261 261 self.log_user()
262 262 user = user_util.create_user()
263 263 user_id = user.user_id
264 264
265 265 self.app.post(
266 266 route_path('edit_user_emails_add', user_id=user_id),
267 267 params={'new_email': 'example@rhodecode.com',
268 268 'csrf_token': self.csrf_token})
269 269
270 270 response = self.app.get(
271 271 route_path('edit_user_emails', user_id=user_id))
272 272 response.mustcontain('example@rhodecode.com')
273 273
274 274 def test_emails_add_existing_email(self, user_util, user_regular):
275 275 existing_email = user_regular.email
276 276
277 277 self.log_user()
278 278 user = user_util.create_user()
279 279 user_id = user.user_id
280 280
281 281 response = self.app.post(
282 282 route_path('edit_user_emails_add', user_id=user_id),
283 283 params={'new_email': existing_email,
284 284 'csrf_token': self.csrf_token})
285 285 assert_session_flash(
286 286 response, 'This e-mail address is already taken')
287 287
288 288 response = self.app.get(
289 289 route_path('edit_user_emails', user_id=user_id))
290 290 response.mustcontain(no=[existing_email])
291 291
292 292 def test_emails_delete(self, user_util):
293 293 self.log_user()
294 294 user = user_util.create_user()
295 295 user_id = user.user_id
296 296
297 297 self.app.post(
298 298 route_path('edit_user_emails_add', user_id=user_id),
299 299 params={'new_email': 'example@rhodecode.com',
300 300 'csrf_token': self.csrf_token})
301 301
302 302 response = self.app.get(
303 303 route_path('edit_user_emails', user_id=user_id))
304 304 response.mustcontain('example@rhodecode.com')
305 305
306 306 user_email = UserEmailMap.query()\
307 307 .filter(UserEmailMap.email == 'example@rhodecode.com') \
308 308 .filter(UserEmailMap.user_id == user_id)\
309 309 .one()
310 310
311 311 del_email_id = user_email.email_id
312 312 self.app.post(
313 313 route_path('edit_user_emails_delete', user_id=user_id),
314 314 params={'del_email_id': del_email_id,
315 315 'csrf_token': self.csrf_token})
316 316
317 317 response = self.app.get(
318 318 route_path('edit_user_emails', user_id=user_id))
319 319 response.mustcontain(no=['example@rhodecode.com'])
320 320
321 321
322 322 def test_create(self, request, xhr_header):
323 323 self.log_user()
324 324 username = 'newtestuser'
325 325 password = 'test12'
326 326 password_confirmation = password
327 327 name = 'name'
328 328 lastname = 'lastname'
329 329 email = 'mail@mail.com'
330 330
331 331 self.app.get(route_path('users_new'))
332 332
333 333 response = self.app.post(route_path('users_create'), params={
334 334 'username': username,
335 335 'password': password,
336 336 'password_confirmation': password_confirmation,
337 337 'firstname': name,
338 338 'active': True,
339 339 'lastname': lastname,
340 340 'extern_name': 'rhodecode',
341 341 'extern_type': 'rhodecode',
342 342 'email': email,
343 343 'csrf_token': self.csrf_token,
344 344 })
345 345 user_link = h.link_to(
346 346 username,
347 347 route_path(
348 348 'user_edit', user_id=User.get_by_username(username).user_id))
349 349 assert_session_flash(response, 'Created user %s' % (user_link,))
350 350
351 351 @request.addfinalizer
352 352 def cleanup():
353 353 fixture.destroy_user(username)
354 354 Session().commit()
355 355
356 356 new_user = User.query().filter(User.username == username).one()
357 357
358 358 assert new_user.username == username
359 359 assert auth.check_password(password, new_user.password)
360 360 assert new_user.name == name
361 361 assert new_user.lastname == lastname
362 362 assert new_user.email == email
363 363
364 364 response = self.app.get(route_path('users_data'),
365 365 extra_environ=xhr_header)
366 366 response.mustcontain(username)
367 367
368 368 def test_create_err(self):
369 369 self.log_user()
370 370 username = 'new_user'
371 371 password = ''
372 372 name = 'name'
373 373 lastname = 'lastname'
374 374 email = 'errmail.com'
375 375
376 376 self.app.get(route_path('users_new'))
377 377
378 378 response = self.app.post(route_path('users_create'), params={
379 379 'username': username,
380 380 'password': password,
381 381 'name': name,
382 382 'active': False,
383 383 'lastname': lastname,
384 384 'email': email,
385 385 'csrf_token': self.csrf_token,
386 386 })
387 387
388 msg = '???'
388 msg = u'Username "%(username)s" is forbidden'
389 389 msg = h.html_escape(msg % {'username': 'new_user'})
390 390 response.mustcontain('<span class="error-message">%s</span>' % msg)
391 391 response.mustcontain(
392 392 '<span class="error-message">Please enter a value</span>')
393 393 response.mustcontain(
394 394 '<span class="error-message">An email address must contain a'
395 395 ' single @</span>')
396 396
397 397 def get_user():
398 398 Session().query(User).filter(User.username == username).one()
399 399
400 400 with pytest.raises(NoResultFound):
401 401 get_user()
402 402
403 403 def test_new(self):
404 404 self.log_user()
405 405 self.app.get(route_path('users_new'))
406 406
407 407 @pytest.mark.parametrize("name, attrs", [
408 408 ('firstname', {'firstname': 'new_username'}),
409 409 ('lastname', {'lastname': 'new_username'}),
410 410 ('admin', {'admin': True}),
411 411 ('admin', {'admin': False}),
412 412 ('extern_type', {'extern_type': 'ldap'}),
413 413 ('extern_type', {'extern_type': None}),
414 414 ('extern_name', {'extern_name': 'test'}),
415 415 ('extern_name', {'extern_name': None}),
416 416 ('active', {'active': False}),
417 417 ('active', {'active': True}),
418 418 ('email', {'email': 'some@email.com'}),
419 419 ('language', {'language': 'de'}),
420 420 ('language', {'language': 'en'}),
421 421 # ('new_password', {'new_password': 'foobar123',
422 422 # 'password_confirmation': 'foobar123'})
423 423 ])
424 424 def test_update(self, name, attrs, user_util):
425 425 self.log_user()
426 426 usr = user_util.create_user(
427 427 password='qweqwe',
428 428 email='testme@rhodecode.org',
429 429 extern_type='rhodecode',
430 430 extern_name='xxx',
431 431 )
432 432 user_id = usr.user_id
433 433 Session().commit()
434 434
435 435 params = usr.get_api_data()
436 436 cur_lang = params['language'] or 'en'
437 437 params.update({
438 438 'password_confirmation': '',
439 439 'new_password': '',
440 440 'language': cur_lang,
441 441 'csrf_token': self.csrf_token,
442 442 })
443 443 params.update({'new_password': ''})
444 444 params.update(attrs)
445 445 if name == 'email':
446 446 params['emails'] = [attrs['email']]
447 447 elif name == 'extern_type':
448 448 # cannot update this via form, expected value is original one
449 449 params['extern_type'] = "rhodecode"
450 450 elif name == 'extern_name':
451 451 # cannot update this via form, expected value is original one
452 452 params['extern_name'] = 'xxx'
453 453 # special case since this user is not
454 454 # logged in yet his data is not filled
455 455 # so we use creation data
456 456
457 457 response = self.app.post(
458 458 route_path('user_update', user_id=usr.user_id), params)
459 459 assert response.status_int == 302
460 460 assert_session_flash(response, 'User updated successfully')
461 461
462 462 updated_user = User.get(user_id)
463 463 updated_params = updated_user.get_api_data()
464 464 updated_params.update({'password_confirmation': ''})
465 465 updated_params.update({'new_password': ''})
466 466
467 467 del params['csrf_token']
468 468 assert params == updated_params
469 469
470 470 def test_update_and_migrate_password(
471 471 self, autologin_user, real_crypto_backend, user_util):
472 472
473 473 user = user_util.create_user()
474 474 temp_user = user.username
475 475 user.password = auth._RhodeCodeCryptoSha256().hash_create(
476 476 b'test123')
477 477 Session().add(user)
478 478 Session().commit()
479 479
480 480 params = user.get_api_data()
481 481
482 482 params.update({
483 483 'password_confirmation': 'qweqwe123',
484 484 'new_password': 'qweqwe123',
485 485 'language': 'en',
486 486 'csrf_token': autologin_user.csrf_token,
487 487 })
488 488
489 489 response = self.app.post(
490 490 route_path('user_update', user_id=user.user_id), params)
491 491 assert response.status_int == 302
492 492 assert_session_flash(response, 'User updated successfully')
493 493
494 494 # new password should be bcrypted, after log-in and transfer
495 495 user = User.get_by_username(temp_user)
496 496 assert user.password.startswith('$')
497 497
498 498 updated_user = User.get_by_username(temp_user)
499 499 updated_params = updated_user.get_api_data()
500 500 updated_params.update({'password_confirmation': 'qweqwe123'})
501 501 updated_params.update({'new_password': 'qweqwe123'})
502 502
503 503 del params['csrf_token']
504 504 assert params == updated_params
505 505
506 506 def test_delete(self):
507 507 self.log_user()
508 508 username = 'newtestuserdeleteme'
509 509
510 510 fixture.create_user(name=username)
511 511
512 512 new_user = Session().query(User)\
513 513 .filter(User.username == username).one()
514 514 response = self.app.post(
515 515 route_path('user_delete', user_id=new_user.user_id),
516 516 params={'csrf_token': self.csrf_token})
517 517
518 518 assert_session_flash(response, 'Successfully deleted user')
519 519
520 520 def test_delete_owner_of_repository(self, request, user_util):
521 521 self.log_user()
522 522 obj_name = 'test_repo'
523 523 usr = user_util.create_user()
524 524 username = usr.username
525 525 fixture.create_repo(obj_name, cur_user=usr.username)
526 526
527 527 new_user = Session().query(User)\
528 528 .filter(User.username == username).one()
529 529 response = self.app.post(
530 530 route_path('user_delete', user_id=new_user.user_id),
531 531 params={'csrf_token': self.csrf_token})
532 532
533 533 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
534 534 'Switch owners or remove those repositories:%s' % (username,
535 535 obj_name)
536 536 assert_session_flash(response, msg)
537 537 fixture.destroy_repo(obj_name)
538 538
539 539 def test_delete_owner_of_repository_detaching(self, request, user_util):
540 540 self.log_user()
541 541 obj_name = 'test_repo'
542 542 usr = user_util.create_user(auto_cleanup=False)
543 543 username = usr.username
544 544 fixture.create_repo(obj_name, cur_user=usr.username)
545 545
546 546 new_user = Session().query(User)\
547 547 .filter(User.username == username).one()
548 548 response = self.app.post(
549 549 route_path('user_delete', user_id=new_user.user_id),
550 550 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
551 551
552 552 msg = 'Detached 1 repositories'
553 553 assert_session_flash(response, msg)
554 554 fixture.destroy_repo(obj_name)
555 555
556 556 def test_delete_owner_of_repository_deleting(self, request, user_util):
557 557 self.log_user()
558 558 obj_name = 'test_repo'
559 559 usr = user_util.create_user(auto_cleanup=False)
560 560 username = usr.username
561 561 fixture.create_repo(obj_name, cur_user=usr.username)
562 562
563 563 new_user = Session().query(User)\
564 564 .filter(User.username == username).one()
565 565 response = self.app.post(
566 566 route_path('user_delete', user_id=new_user.user_id),
567 567 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
568 568
569 569 msg = 'Deleted 1 repositories'
570 570 assert_session_flash(response, msg)
571 571
572 572 def test_delete_owner_of_repository_group(self, request, user_util):
573 573 self.log_user()
574 574 obj_name = 'test_group'
575 575 usr = user_util.create_user()
576 576 username = usr.username
577 577 fixture.create_repo_group(obj_name, cur_user=usr.username)
578 578
579 579 new_user = Session().query(User)\
580 580 .filter(User.username == username).one()
581 581 response = self.app.post(
582 582 route_path('user_delete', user_id=new_user.user_id),
583 583 params={'csrf_token': self.csrf_token})
584 584
585 585 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
586 586 'Switch owners or remove those repository groups:%s' % (username,
587 587 obj_name)
588 588 assert_session_flash(response, msg)
589 589 fixture.destroy_repo_group(obj_name)
590 590
591 591 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
592 592 self.log_user()
593 593 obj_name = 'test_group'
594 594 usr = user_util.create_user(auto_cleanup=False)
595 595 username = usr.username
596 596 fixture.create_repo_group(obj_name, cur_user=usr.username)
597 597
598 598 new_user = Session().query(User)\
599 599 .filter(User.username == username).one()
600 600 response = self.app.post(
601 601 route_path('user_delete', user_id=new_user.user_id),
602 602 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
603 603
604 604 msg = 'Deleted 1 repository groups'
605 605 assert_session_flash(response, msg)
606 606
607 607 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
608 608 self.log_user()
609 609 obj_name = 'test_group'
610 610 usr = user_util.create_user(auto_cleanup=False)
611 611 username = usr.username
612 612 fixture.create_repo_group(obj_name, cur_user=usr.username)
613 613
614 614 new_user = Session().query(User)\
615 615 .filter(User.username == username).one()
616 616 response = self.app.post(
617 617 route_path('user_delete', user_id=new_user.user_id),
618 618 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
619 619
620 620 msg = 'Detached 1 repository groups'
621 621 assert_session_flash(response, msg)
622 622 fixture.destroy_repo_group(obj_name)
623 623
624 624 def test_delete_owner_of_user_group(self, request, user_util):
625 625 self.log_user()
626 626 obj_name = 'test_user_group'
627 627 usr = user_util.create_user()
628 628 username = usr.username
629 629 fixture.create_user_group(obj_name, cur_user=usr.username)
630 630
631 631 new_user = Session().query(User)\
632 632 .filter(User.username == username).one()
633 633 response = self.app.post(
634 634 route_path('user_delete', user_id=new_user.user_id),
635 635 params={'csrf_token': self.csrf_token})
636 636
637 637 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
638 638 'Switch owners or remove those user groups:%s' % (username,
639 639 obj_name)
640 640 assert_session_flash(response, msg)
641 641 fixture.destroy_user_group(obj_name)
642 642
643 643 def test_delete_owner_of_user_group_detaching(self, request, user_util):
644 644 self.log_user()
645 645 obj_name = 'test_user_group'
646 646 usr = user_util.create_user(auto_cleanup=False)
647 647 username = usr.username
648 648 fixture.create_user_group(obj_name, cur_user=usr.username)
649 649
650 650 new_user = Session().query(User)\
651 651 .filter(User.username == username).one()
652 652 try:
653 653 response = self.app.post(
654 654 route_path('user_delete', user_id=new_user.user_id),
655 655 params={'user_user_groups': 'detach',
656 656 'csrf_token': self.csrf_token})
657 657
658 658 msg = 'Detached 1 user groups'
659 659 assert_session_flash(response, msg)
660 660 finally:
661 661 fixture.destroy_user_group(obj_name)
662 662
663 663 def test_delete_owner_of_user_group_deleting(self, request, user_util):
664 664 self.log_user()
665 665 obj_name = 'test_user_group'
666 666 usr = user_util.create_user(auto_cleanup=False)
667 667 username = usr.username
668 668 fixture.create_user_group(obj_name, cur_user=usr.username)
669 669
670 670 new_user = Session().query(User)\
671 671 .filter(User.username == username).one()
672 672 response = self.app.post(
673 673 route_path('user_delete', user_id=new_user.user_id),
674 674 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
675 675
676 676 msg = 'Deleted 1 user groups'
677 677 assert_session_flash(response, msg)
678 678
679 679 def test_edit(self, user_util):
680 680 self.log_user()
681 681 user = user_util.create_user()
682 682 self.app.get(route_path('user_edit', user_id=user.user_id))
683 683
684 684 def test_edit_default_user_redirect(self):
685 685 self.log_user()
686 686 user = User.get_default_user()
687 687 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
688 688
689 689 @pytest.mark.parametrize(
690 690 'repo_create, repo_create_write, user_group_create, repo_group_create,'
691 691 'fork_create, inherit_default_permissions, expect_error,'
692 692 'expect_form_error', [
693 693 ('hg.create.none', 'hg.create.write_on_repogroup.false',
694 694 'hg.usergroup.create.false', 'hg.repogroup.create.false',
695 695 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
696 696 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
697 697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 699 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
700 700 'hg.usergroup.create.true', 'hg.repogroup.create.true',
701 701 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
702 702 False),
703 703 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
704 704 'hg.usergroup.create.true', 'hg.repogroup.create.true',
705 705 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
706 706 True),
707 707 ('', '', '', '', '', '', True, False),
708 708 ])
709 709 def test_global_perms_on_user(
710 710 self, repo_create, repo_create_write, user_group_create,
711 711 repo_group_create, fork_create, expect_error, expect_form_error,
712 712 inherit_default_permissions, user_util):
713 713 self.log_user()
714 714 user = user_util.create_user()
715 715 uid = user.user_id
716 716
717 717 # ENABLE REPO CREATE ON A GROUP
718 718 perm_params = {
719 719 'inherit_default_permissions': False,
720 720 'default_repo_create': repo_create,
721 721 'default_repo_create_on_write': repo_create_write,
722 722 'default_user_group_create': user_group_create,
723 723 'default_repo_group_create': repo_group_create,
724 724 'default_fork_create': fork_create,
725 725 'default_inherit_default_permissions': inherit_default_permissions,
726 726 'csrf_token': self.csrf_token,
727 727 }
728 728 response = self.app.post(
729 729 route_path('user_edit_global_perms_update', user_id=uid),
730 730 params=perm_params)
731 731
732 732 if expect_form_error:
733 733 assert response.status_int == 200
734 734 response.mustcontain('Value must be one of')
735 735 else:
736 736 if expect_error:
737 737 msg = 'An error occurred during permissions saving'
738 738 else:
739 739 msg = 'User global permissions updated successfully'
740 740 ug = User.get(uid)
741 741 del perm_params['inherit_default_permissions']
742 742 del perm_params['csrf_token']
743 743 assert perm_params == ug.get_default_perms()
744 744 assert_session_flash(response, msg)
745 745
746 746 def test_global_permissions_initial_values(self, user_util):
747 747 self.log_user()
748 748 user = user_util.create_user()
749 749 uid = user.user_id
750 750 response = self.app.get(
751 751 route_path('user_edit_global_perms', user_id=uid))
752 752 default_user = User.get_default_user()
753 753 default_permissions = default_user.get_default_perms()
754 754 assert_response = response.assert_response()
755 755 expected_permissions = (
756 756 'default_repo_create', 'default_repo_create_on_write',
757 757 'default_fork_create', 'default_repo_group_create',
758 758 'default_user_group_create', 'default_inherit_default_permissions')
759 759 for permission in expected_permissions:
760 760 css_selector = '[name={}][checked=checked]'.format(permission)
761 761 element = assert_response.get_element(css_selector)
762 762 assert element.value == default_permissions[permission]
763 763
764 764 def test_perms_summary_page(self):
765 765 user = self.log_user()
766 766 response = self.app.get(
767 767 route_path('edit_user_perms_summary', user_id=user['user_id']))
768 768 for repo in Repository.query().all():
769 769 response.mustcontain(repo.repo_name)
770 770
771 771 def test_perms_summary_page_json(self):
772 772 user = self.log_user()
773 773 response = self.app.get(
774 774 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
775 775 for repo in Repository.query().all():
776 776 response.mustcontain(repo.repo_name)
777 777
778 778 def test_audit_log_page(self):
779 779 user = self.log_user()
780 780 self.app.get(
781 781 route_path('edit_user_audit_logs', user_id=user['user_id']))
@@ -1,486 +1,484 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 re
22 22 import logging
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import datetime
26 26 from pyramid.interfaces import IRoutesMapper
27 27
28 28 from pyramid.view import view_config
29 29 from pyramid.httpexceptions import HTTPFound
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 34 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
35 35 from rhodecode.events import trigger
36 36
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 40 from rhodecode.lib.utils2 import aslist, safe_unicode
41 41 from rhodecode.model.db import (
42 42 or_, coalesce, User, UserIpMap, UserSshKeys)
43 43 from rhodecode.model.forms import (
44 44 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.permission import PermissionModel
47 47 from rhodecode.model.settings import SettingsModel
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class AdminPermissionsView(BaseAppView, DataGridAppView):
54 54 def load_default_context(self):
55 55 c = self._get_local_tmpl_context()
56
57
58 56 PermissionModel().set_global_permission_choices(
59 57 c, gettext_translator=self.request.translate)
60 58 return c
61 59
62 60 @LoginRequired()
63 61 @HasPermissionAllDecorator('hg.admin')
64 62 @view_config(
65 63 route_name='admin_permissions_application', request_method='GET',
66 64 renderer='rhodecode:templates/admin/permissions/permissions.mako')
67 65 def permissions_application(self):
68 66 c = self.load_default_context()
69 67 c.active = 'application'
70 68
71 69 c.user = User.get_default_user(refresh=True)
72 70
73 71 app_settings = SettingsModel().get_all_settings()
74 72 defaults = {
75 73 'anonymous': c.user.active,
76 74 'default_register_message': app_settings.get(
77 75 'rhodecode_register_message')
78 76 }
79 77 defaults.update(c.user.get_default_perms())
80 78
81 79 data = render('rhodecode:templates/admin/permissions/permissions.mako',
82 80 self._get_template_context(c), self.request)
83 81 html = formencode.htmlfill.render(
84 82 data,
85 83 defaults=defaults,
86 84 encoding="UTF-8",
87 85 force_defaults=False
88 86 )
89 87 return Response(html)
90 88
91 89 @LoginRequired()
92 90 @HasPermissionAllDecorator('hg.admin')
93 91 @CSRFRequired()
94 92 @view_config(
95 93 route_name='admin_permissions_application_update', request_method='POST',
96 94 renderer='rhodecode:templates/admin/permissions/permissions.mako')
97 95 def permissions_application_update(self):
98 96 _ = self.request.translate
99 97 c = self.load_default_context()
100 98 c.active = 'application'
101 99
102 100 _form = ApplicationPermissionsForm(
103 101 self.request.translate,
104 102 [x[0] for x in c.register_choices],
105 103 [x[0] for x in c.password_reset_choices],
106 104 [x[0] for x in c.extern_activate_choices])()
107 105
108 106 try:
109 107 form_result = _form.to_python(dict(self.request.POST))
110 108 form_result.update({'perm_user_name': User.DEFAULT_USER})
111 109 PermissionModel().update_application_permissions(form_result)
112 110
113 111 settings = [
114 112 ('register_message', 'default_register_message'),
115 113 ]
116 114 for setting, form_key in settings:
117 115 sett = SettingsModel().create_or_update_setting(
118 116 setting, form_result[form_key])
119 117 Session().add(sett)
120 118
121 119 Session().commit()
122 120 h.flash(_('Application permissions updated successfully'),
123 121 category='success')
124 122
125 123 except formencode.Invalid as errors:
126 124 defaults = errors.value
127 125
128 126 data = render(
129 127 'rhodecode:templates/admin/permissions/permissions.mako',
130 128 self._get_template_context(c), self.request)
131 129 html = formencode.htmlfill.render(
132 130 data,
133 131 defaults=defaults,
134 132 errors=errors.error_dict or {},
135 133 prefix_error=False,
136 134 encoding="UTF-8",
137 135 force_defaults=False
138 136 )
139 137 return Response(html)
140 138
141 139 except Exception:
142 140 log.exception("Exception during update of permissions")
143 141 h.flash(_('Error occurred during update of permissions'),
144 142 category='error')
145 143
146 144 raise HTTPFound(h.route_path('admin_permissions_application'))
147 145
148 146 @LoginRequired()
149 147 @HasPermissionAllDecorator('hg.admin')
150 148 @view_config(
151 149 route_name='admin_permissions_object', request_method='GET',
152 150 renderer='rhodecode:templates/admin/permissions/permissions.mako')
153 151 def permissions_objects(self):
154 152 c = self.load_default_context()
155 153 c.active = 'objects'
156 154
157 155 c.user = User.get_default_user(refresh=True)
158 156 defaults = {}
159 157 defaults.update(c.user.get_default_perms())
160 158
161 159 data = render(
162 160 'rhodecode:templates/admin/permissions/permissions.mako',
163 161 self._get_template_context(c), self.request)
164 162 html = formencode.htmlfill.render(
165 163 data,
166 164 defaults=defaults,
167 165 encoding="UTF-8",
168 166 force_defaults=False
169 167 )
170 168 return Response(html)
171 169
172 170 @LoginRequired()
173 171 @HasPermissionAllDecorator('hg.admin')
174 172 @CSRFRequired()
175 173 @view_config(
176 174 route_name='admin_permissions_object_update', request_method='POST',
177 175 renderer='rhodecode:templates/admin/permissions/permissions.mako')
178 176 def permissions_objects_update(self):
179 177 _ = self.request.translate
180 178 c = self.load_default_context()
181 179 c.active = 'objects'
182 180
183 181 _form = ObjectPermissionsForm(
184 182 self.request.translate,
185 183 [x[0] for x in c.repo_perms_choices],
186 184 [x[0] for x in c.group_perms_choices],
187 185 [x[0] for x in c.user_group_perms_choices])()
188 186
189 187 try:
190 188 form_result = _form.to_python(dict(self.request.POST))
191 189 form_result.update({'perm_user_name': User.DEFAULT_USER})
192 190 PermissionModel().update_object_permissions(form_result)
193 191
194 192 Session().commit()
195 193 h.flash(_('Object permissions updated successfully'),
196 194 category='success')
197 195
198 196 except formencode.Invalid as errors:
199 197 defaults = errors.value
200 198
201 199 data = render(
202 200 'rhodecode:templates/admin/permissions/permissions.mako',
203 201 self._get_template_context(c), self.request)
204 202 html = formencode.htmlfill.render(
205 203 data,
206 204 defaults=defaults,
207 205 errors=errors.error_dict or {},
208 206 prefix_error=False,
209 207 encoding="UTF-8",
210 208 force_defaults=False
211 209 )
212 210 return Response(html)
213 211 except Exception:
214 212 log.exception("Exception during update of permissions")
215 213 h.flash(_('Error occurred during update of permissions'),
216 214 category='error')
217 215
218 216 raise HTTPFound(h.route_path('admin_permissions_object'))
219 217
220 218 @LoginRequired()
221 219 @HasPermissionAllDecorator('hg.admin')
222 220 @view_config(
223 221 route_name='admin_permissions_global', request_method='GET',
224 222 renderer='rhodecode:templates/admin/permissions/permissions.mako')
225 223 def permissions_global(self):
226 224 c = self.load_default_context()
227 225 c.active = 'global'
228 226
229 227 c.user = User.get_default_user(refresh=True)
230 228 defaults = {}
231 229 defaults.update(c.user.get_default_perms())
232 230
233 231 data = render(
234 232 'rhodecode:templates/admin/permissions/permissions.mako',
235 233 self._get_template_context(c), self.request)
236 234 html = formencode.htmlfill.render(
237 235 data,
238 236 defaults=defaults,
239 237 encoding="UTF-8",
240 238 force_defaults=False
241 239 )
242 240 return Response(html)
243 241
244 242 @LoginRequired()
245 243 @HasPermissionAllDecorator('hg.admin')
246 244 @CSRFRequired()
247 245 @view_config(
248 246 route_name='admin_permissions_global_update', request_method='POST',
249 247 renderer='rhodecode:templates/admin/permissions/permissions.mako')
250 248 def permissions_global_update(self):
251 249 _ = self.request.translate
252 250 c = self.load_default_context()
253 251 c.active = 'global'
254 252
255 253 _form = UserPermissionsForm(
256 254 self.request.translate,
257 255 [x[0] for x in c.repo_create_choices],
258 256 [x[0] for x in c.repo_create_on_write_choices],
259 257 [x[0] for x in c.repo_group_create_choices],
260 258 [x[0] for x in c.user_group_create_choices],
261 259 [x[0] for x in c.fork_choices],
262 260 [x[0] for x in c.inherit_default_permission_choices])()
263 261
264 262 try:
265 263 form_result = _form.to_python(dict(self.request.POST))
266 264 form_result.update({'perm_user_name': User.DEFAULT_USER})
267 265 PermissionModel().update_user_permissions(form_result)
268 266
269 267 Session().commit()
270 268 h.flash(_('Global permissions updated successfully'),
271 269 category='success')
272 270
273 271 except formencode.Invalid as errors:
274 272 defaults = errors.value
275 273
276 274 data = render(
277 275 'rhodecode:templates/admin/permissions/permissions.mako',
278 276 self._get_template_context(c), self.request)
279 277 html = formencode.htmlfill.render(
280 278 data,
281 279 defaults=defaults,
282 280 errors=errors.error_dict or {},
283 281 prefix_error=False,
284 282 encoding="UTF-8",
285 283 force_defaults=False
286 284 )
287 285 return Response(html)
288 286 except Exception:
289 287 log.exception("Exception during update of permissions")
290 288 h.flash(_('Error occurred during update of permissions'),
291 289 category='error')
292 290
293 291 raise HTTPFound(h.route_path('admin_permissions_global'))
294 292
295 293 @LoginRequired()
296 294 @HasPermissionAllDecorator('hg.admin')
297 295 @view_config(
298 296 route_name='admin_permissions_ips', request_method='GET',
299 297 renderer='rhodecode:templates/admin/permissions/permissions.mako')
300 298 def permissions_ips(self):
301 299 c = self.load_default_context()
302 300 c.active = 'ips'
303 301
304 302 c.user = User.get_default_user(refresh=True)
305 303 c.user_ip_map = (
306 304 UserIpMap.query().filter(UserIpMap.user == c.user).all())
307 305
308 306 return self._get_template_context(c)
309 307
310 308 @LoginRequired()
311 309 @HasPermissionAllDecorator('hg.admin')
312 310 @view_config(
313 311 route_name='admin_permissions_overview', request_method='GET',
314 312 renderer='rhodecode:templates/admin/permissions/permissions.mako')
315 313 def permissions_overview(self):
316 314 c = self.load_default_context()
317 315 c.active = 'perms'
318 316
319 317 c.user = User.get_default_user(refresh=True)
320 318 c.perm_user = c.user.AuthUser()
321 319 return self._get_template_context(c)
322 320
323 321 @LoginRequired()
324 322 @HasPermissionAllDecorator('hg.admin')
325 323 @view_config(
326 324 route_name='admin_permissions_auth_token_access', request_method='GET',
327 325 renderer='rhodecode:templates/admin/permissions/permissions.mako')
328 326 def auth_token_access(self):
329 327 from rhodecode import CONFIG
330 328
331 329 c = self.load_default_context()
332 330 c.active = 'auth_token_access'
333 331
334 332 c.user = User.get_default_user(refresh=True)
335 333 c.perm_user = c.user.AuthUser()
336 334
337 335 mapper = self.request.registry.queryUtility(IRoutesMapper)
338 336 c.view_data = []
339 337
340 338 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
341 339 introspector = self.request.registry.introspector
342 340
343 341 view_intr = {}
344 342 for view_data in introspector.get_category('views'):
345 343 intr = view_data['introspectable']
346 344
347 345 if 'route_name' in intr and intr['attr']:
348 346 view_intr[intr['route_name']] = '{}:{}'.format(
349 347 str(intr['derived_callable'].func_name), intr['attr']
350 348 )
351 349
352 350 c.whitelist_key = 'api_access_controllers_whitelist'
353 351 c.whitelist_file = CONFIG.get('__file__')
354 352 whitelist_views = aslist(
355 353 CONFIG.get(c.whitelist_key), sep=',')
356 354
357 355 for route_info in mapper.get_routes():
358 356 if not route_info.name.startswith('__'):
359 357 routepath = route_info.pattern
360 358
361 359 def replace(matchobj):
362 360 if matchobj.group(1):
363 361 return "{%s}" % matchobj.group(1).split(':')[0]
364 362 else:
365 363 return "{%s}" % matchobj.group(2)
366 364
367 365 routepath = _argument_prog.sub(replace, routepath)
368 366
369 367 if not routepath.startswith('/'):
370 368 routepath = '/' + routepath
371 369
372 370 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
373 371 active = view_fqn in whitelist_views
374 372 c.view_data.append((route_info.name, view_fqn, routepath, active))
375 373
376 374 c.whitelist_views = whitelist_views
377 375 return self._get_template_context(c)
378 376
379 377 def ssh_enabled(self):
380 378 return self.request.registry.settings.get(
381 379 'ssh.generate_authorized_keyfile')
382 380
383 381 @LoginRequired()
384 382 @HasPermissionAllDecorator('hg.admin')
385 383 @view_config(
386 384 route_name='admin_permissions_ssh_keys', request_method='GET',
387 385 renderer='rhodecode:templates/admin/permissions/permissions.mako')
388 386 def ssh_keys(self):
389 387 c = self.load_default_context()
390 388 c.active = 'ssh_keys'
391 389 c.ssh_enabled = self.ssh_enabled()
392 390 return self._get_template_context(c)
393 391
394 392 @LoginRequired()
395 393 @HasPermissionAllDecorator('hg.admin')
396 394 @view_config(
397 395 route_name='admin_permissions_ssh_keys_data', request_method='GET',
398 396 renderer='json_ext', xhr=True)
399 397 def ssh_keys_data(self):
400 398 _ = self.request.translate
401 399 self.load_default_context()
402 400 column_map = {
403 401 'fingerprint': 'ssh_key_fingerprint',
404 402 'username': User.username
405 403 }
406 404 draw, start, limit = self._extract_chunk(self.request)
407 405 search_q, order_by, order_dir = self._extract_ordering(
408 406 self.request, column_map=column_map)
409 407
410 408 ssh_keys_data_total_count = UserSshKeys.query()\
411 409 .count()
412 410
413 411 # json generate
414 412 base_q = UserSshKeys.query().join(UserSshKeys.user)
415 413
416 414 if search_q:
417 415 like_expression = u'%{}%'.format(safe_unicode(search_q))
418 416 base_q = base_q.filter(or_(
419 417 User.username.ilike(like_expression),
420 418 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
421 419 ))
422 420
423 421 users_data_total_filtered_count = base_q.count()
424 422
425 423 sort_col = self._get_order_col(order_by, UserSshKeys)
426 424 if sort_col:
427 425 if order_dir == 'asc':
428 426 # handle null values properly to order by NULL last
429 427 if order_by in ['created_on']:
430 428 sort_col = coalesce(sort_col, datetime.date.max)
431 429 sort_col = sort_col.asc()
432 430 else:
433 431 # handle null values properly to order by NULL last
434 432 if order_by in ['created_on']:
435 433 sort_col = coalesce(sort_col, datetime.date.min)
436 434 sort_col = sort_col.desc()
437 435
438 436 base_q = base_q.order_by(sort_col)
439 437 base_q = base_q.offset(start).limit(limit)
440 438
441 439 ssh_keys = base_q.all()
442 440
443 441 ssh_keys_data = []
444 442 for ssh_key in ssh_keys:
445 443 ssh_keys_data.append({
446 444 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
447 445 "fingerprint": ssh_key.ssh_key_fingerprint,
448 446 "description": ssh_key.description,
449 447 "created_on": h.format_date(ssh_key.created_on),
450 448 "accessed_on": h.format_date(ssh_key.accessed_on),
451 449 "action": h.link_to(
452 450 _('Edit'), h.route_path('edit_user_ssh_keys',
453 451 user_id=ssh_key.user.user_id))
454 452 })
455 453
456 454 data = ({
457 455 'draw': draw,
458 456 'data': ssh_keys_data,
459 457 'recordsTotal': ssh_keys_data_total_count,
460 458 'recordsFiltered': users_data_total_filtered_count,
461 459 })
462 460
463 461 return data
464 462
465 463 @LoginRequired()
466 464 @HasPermissionAllDecorator('hg.admin')
467 465 @CSRFRequired()
468 466 @view_config(
469 467 route_name='admin_permissions_ssh_keys_update', request_method='POST',
470 468 renderer='rhodecode:templates/admin/permissions/permissions.mako')
471 469 def ssh_keys_update(self):
472 470 _ = self.request.translate
473 471 self.load_default_context()
474 472
475 473 ssh_enabled = self.ssh_enabled()
476 474 key_file = self.request.registry.settings.get(
477 475 'ssh.authorized_keys_file_path')
478 476 if ssh_enabled:
479 477 trigger(SshKeyFileChangeEvent(), self.request.registry)
480 478 h.flash(_('Updated SSH keys file: {}').format(key_file),
481 479 category='success')
482 480 else:
483 481 h.flash(_('SSH key support is disabled in .ini file'),
484 482 category='warning')
485 483
486 484 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,183 +1,183 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, CSRFRequired, NotAnonymous,
35 35 HasPermissionAny, HasRepoGroupPermissionAny)
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.utils import repo_name_slug
38 38 from rhodecode.lib.utils2 import safe_int, safe_unicode
39 39 from rhodecode.model.forms import RepoForm
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
42 42 from rhodecode.model.settings import SettingsModel
43 43 from rhodecode.model.db import Repository, RepoGroup
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class AdminReposView(BaseAppView, DataGridAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context()
52 52
53 53 return c
54 54
55 55 def _load_form_data(self, c):
56 56 acl_groups = RepoGroupList(RepoGroup.query().all(),
57 57 perm_set=['group.write', 'group.admin'])
58 58 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
59 59 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
60 60 c.landing_revs_choices, c.landing_revs = \
61 ScmModel().get_repo_landing_revs()
61 ScmModel().get_repo_landing_revs(self.request.translate)
62 62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63 63
64 64 @LoginRequired()
65 65 @NotAnonymous()
66 66 # perms check inside
67 67 @view_config(
68 68 route_name='repos', request_method='GET',
69 69 renderer='rhodecode:templates/admin/repos/repos.mako')
70 70 def repository_list(self):
71 71 c = self.load_default_context()
72 72
73 73 repo_list = Repository.get_all_repos()
74 74 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
75 75 repos_data = RepoModel().get_repos_as_dict(
76 76 repo_list=c.repo_list, admin=True, super_user_actions=True)
77 77 # json used to render the grid
78 78 c.data = json.dumps(repos_data)
79 79
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @NotAnonymous()
84 84 # perms check inside
85 85 @view_config(
86 86 route_name='repo_new', request_method='GET',
87 87 renderer='rhodecode:templates/admin/repos/repo_add.mako')
88 88 def repository_new(self):
89 89 c = self.load_default_context()
90 90
91 91 new_repo = self.request.GET.get('repo', '')
92 92 parent_group = safe_int(self.request.GET.get('parent_group'))
93 93 _gr = RepoGroup.get(parent_group)
94 94
95 95 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
96 96 # you're not super admin nor have global create permissions,
97 97 # but maybe you have at least write permission to a parent group ?
98 98
99 99 gr_name = _gr.group_name if _gr else None
100 100 # create repositories with write permission on group is set to true
101 101 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
102 102 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
103 103 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
104 104 if not (group_admin or (group_write and create_on_write)):
105 105 raise HTTPForbidden()
106 106
107 107 self._load_form_data(c)
108 108 c.new_repo = repo_name_slug(new_repo)
109 109
110 110 # apply the defaults from defaults page
111 111 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
112 112 # set checkbox to autochecked
113 113 defaults['repo_copy_permissions'] = True
114 114
115 115 parent_group_choice = '-1'
116 116 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
117 117 parent_group_choice = self._rhodecode_user.personal_repo_group
118 118
119 119 if parent_group and _gr:
120 120 if parent_group in [x[0] for x in c.repo_groups]:
121 121 parent_group_choice = safe_unicode(parent_group)
122 122
123 123 defaults.update({'repo_group': parent_group_choice})
124 124
125 125 data = render('rhodecode:templates/admin/repos/repo_add.mako',
126 126 self._get_template_context(c), self.request)
127 127 html = formencode.htmlfill.render(
128 128 data,
129 129 defaults=defaults,
130 130 encoding="UTF-8",
131 131 force_defaults=False
132 132 )
133 133 return Response(html)
134 134
135 135 @LoginRequired()
136 136 @NotAnonymous()
137 137 @CSRFRequired()
138 138 # perms check inside
139 139 @view_config(
140 140 route_name='repo_create', request_method='POST',
141 141 renderer='rhodecode:templates/admin/repos/repos.mako')
142 142 def repository_create(self):
143 143 c = self.load_default_context()
144 144
145 145 form_result = {}
146 146 task_id = None
147 147 self._load_form_data(c)
148 148
149 149 try:
150 150 # CanWriteToGroup validators checks permissions of this POST
151 151 form = RepoForm(
152 152 self.request.translate, repo_groups=c.repo_groups_choices,
153 153 landing_revs=c.landing_revs_choices)()
154 154 form_results = form.to_python(dict(self.request.POST))
155 155
156 156 # create is done sometimes async on celery, db transaction
157 157 # management is handled there.
158 158 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
159 159 from celery.result import BaseAsyncResult
160 160 if isinstance(task, BaseAsyncResult):
161 161 task_id = task.task_id
162 162 except formencode.Invalid as errors:
163 163 data = render('rhodecode:templates/admin/repos/repo_add.mako',
164 164 self._get_template_context(c), self.request)
165 165 html = formencode.htmlfill.render(
166 166 data,
167 167 defaults=errors.value,
168 168 errors=errors.error_dict or {},
169 169 prefix_error=False,
170 170 encoding="UTF-8",
171 171 force_defaults=False
172 172 )
173 173 return Response(html)
174 174
175 175 except Exception as e:
176 176 msg = self._log_creation_exception(e, form_result.get('repo_name'))
177 177 h.flash(msg, category='error')
178 178 raise HTTPFound(h.route_path('home'))
179 179
180 180 raise HTTPFound(
181 181 h.route_path('repo_creating',
182 182 repo_name=form_result['repo_name_full'],
183 183 _query=dict(task_id=task_id)))
@@ -1,762 +1,762 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 import logging
23 23 import collections
24 24
25 25 import datetime
26 26 import formencode
27 27 import formencode.htmlfill
28 28
29 29 import rhodecode
30 30 from pyramid.view import view_config
31 31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 from rhodecode.apps._base import BaseAppView
36 36 from rhodecode.apps.admin.navigation import navigation_list
37 37 from rhodecode.apps.svn_support.config_keys import generate_config
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 41 from rhodecode.lib.celerylib import tasks, run_task
42 42 from rhodecode.lib.utils import repo2db_mapper
43 43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 44 from rhodecode.lib.index import searcher_from_config
45 45
46 46 from rhodecode.model.db import RhodeCodeUi, Repository
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70
71 71 return c
72 72
73 73 @classmethod
74 74 def _get_ui_settings(cls):
75 75 ret = RhodeCodeUi.query().all()
76 76
77 77 if not ret:
78 78 raise Exception('Could not get application ui settings !')
79 79 settings = {}
80 80 for each in ret:
81 81 k = each.ui_key
82 82 v = each.ui_value
83 83 if k == '/':
84 84 k = 'root_path'
85 85
86 86 if k in ['push_ssl', 'publish', 'enabled']:
87 87 v = str2bool(v)
88 88
89 89 if k.find('.') != -1:
90 90 k = k.replace('.', '_')
91 91
92 92 if each.ui_section in ['hooks', 'extensions']:
93 93 v = each.ui_active
94 94
95 95 settings[each.ui_section + '_' + k] = v
96 96 return settings
97 97
98 98 @classmethod
99 99 def _form_defaults(cls):
100 100 defaults = SettingsModel().get_all_settings()
101 101 defaults.update(cls._get_ui_settings())
102 102
103 103 defaults.update({
104 104 'new_svn_branch': '',
105 105 'new_svn_tag': '',
106 106 })
107 107 return defaults
108 108
109 109 @LoginRequired()
110 110 @HasPermissionAllDecorator('hg.admin')
111 111 @view_config(
112 112 route_name='admin_settings_vcs', request_method='GET',
113 113 renderer='rhodecode:templates/admin/settings/settings.mako')
114 114 def settings_vcs(self):
115 115 c = self.load_default_context()
116 116 c.active = 'vcs'
117 117 model = VcsSettingsModel()
118 118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120 120
121 121 settings = self.request.registry.settings
122 122 c.svn_proxy_generate_config = settings[generate_config]
123 123
124 124 defaults = self._form_defaults()
125 125
126 126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127 127
128 128 data = render('rhodecode:templates/admin/settings/settings.mako',
129 129 self._get_template_context(c), self.request)
130 130 html = formencode.htmlfill.render(
131 131 data,
132 132 defaults=defaults,
133 133 encoding="UTF-8",
134 134 force_defaults=False
135 135 )
136 136 return Response(html)
137 137
138 138 @LoginRequired()
139 139 @HasPermissionAllDecorator('hg.admin')
140 140 @CSRFRequired()
141 141 @view_config(
142 142 route_name='admin_settings_vcs_update', request_method='POST',
143 143 renderer='rhodecode:templates/admin/settings/settings.mako')
144 144 def settings_vcs_update(self):
145 145 _ = self.request.translate
146 146 c = self.load_default_context()
147 147 c.active = 'vcs'
148 148
149 149 model = VcsSettingsModel()
150 150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152 152
153 153 settings = self.request.registry.settings
154 154 c.svn_proxy_generate_config = settings[generate_config]
155 155
156 156 application_form = ApplicationUiSettingsForm(self.request.translate)()
157 157
158 158 try:
159 159 form_result = application_form.to_python(dict(self.request.POST))
160 160 except formencode.Invalid as errors:
161 161 h.flash(
162 162 _("Some form inputs contain invalid data."),
163 163 category='error')
164 164 data = render('rhodecode:templates/admin/settings/settings.mako',
165 165 self._get_template_context(c), self.request)
166 166 html = formencode.htmlfill.render(
167 167 data,
168 168 defaults=errors.value,
169 169 errors=errors.error_dict or {},
170 170 prefix_error=False,
171 171 encoding="UTF-8",
172 172 force_defaults=False
173 173 )
174 174 return Response(html)
175 175
176 176 try:
177 177 if c.visual.allow_repo_location_change:
178 178 model.update_global_path_setting(
179 179 form_result['paths_root_path'])
180 180
181 181 model.update_global_ssl_setting(form_result['web_push_ssl'])
182 182 model.update_global_hook_settings(form_result)
183 183
184 184 model.create_or_update_global_svn_settings(form_result)
185 185 model.create_or_update_global_hg_settings(form_result)
186 186 model.create_or_update_global_git_settings(form_result)
187 187 model.create_or_update_global_pr_settings(form_result)
188 188 except Exception:
189 189 log.exception("Exception while updating settings")
190 190 h.flash(_('Error occurred during updating '
191 191 'application settings'), category='error')
192 192 else:
193 193 Session().commit()
194 194 h.flash(_('Updated VCS settings'), category='success')
195 195 raise HTTPFound(h.route_path('admin_settings_vcs'))
196 196
197 197 data = render('rhodecode:templates/admin/settings/settings.mako',
198 198 self._get_template_context(c), self.request)
199 199 html = formencode.htmlfill.render(
200 200 data,
201 201 defaults=self._form_defaults(),
202 202 encoding="UTF-8",
203 203 force_defaults=False
204 204 )
205 205 return Response(html)
206 206
207 207 @LoginRequired()
208 208 @HasPermissionAllDecorator('hg.admin')
209 209 @CSRFRequired()
210 210 @view_config(
211 211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
212 212 renderer='json_ext', xhr=True)
213 213 def settings_vcs_delete_svn_pattern(self):
214 214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
215 215 model = VcsSettingsModel()
216 216 try:
217 217 model.delete_global_svn_pattern(delete_pattern_id)
218 218 except SettingNotFound:
219 219 log.exception(
220 220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
221 221 raise HTTPNotFound()
222 222
223 223 Session().commit()
224 224 return True
225 225
226 226 @LoginRequired()
227 227 @HasPermissionAllDecorator('hg.admin')
228 228 @view_config(
229 229 route_name='admin_settings_mapping', request_method='GET',
230 230 renderer='rhodecode:templates/admin/settings/settings.mako')
231 231 def settings_mapping(self):
232 232 c = self.load_default_context()
233 233 c.active = 'mapping'
234 234
235 235 data = render('rhodecode:templates/admin/settings/settings.mako',
236 236 self._get_template_context(c), self.request)
237 237 html = formencode.htmlfill.render(
238 238 data,
239 239 defaults=self._form_defaults(),
240 240 encoding="UTF-8",
241 241 force_defaults=False
242 242 )
243 243 return Response(html)
244 244
245 245 @LoginRequired()
246 246 @HasPermissionAllDecorator('hg.admin')
247 247 @CSRFRequired()
248 248 @view_config(
249 249 route_name='admin_settings_mapping_update', request_method='POST',
250 250 renderer='rhodecode:templates/admin/settings/settings.mako')
251 251 def settings_mapping_update(self):
252 252 _ = self.request.translate
253 253 c = self.load_default_context()
254 254 c.active = 'mapping'
255 255 rm_obsolete = self.request.POST.get('destroy', False)
256 256 invalidate_cache = self.request.POST.get('invalidate', False)
257 257 log.debug(
258 258 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
259 259
260 260 if invalidate_cache:
261 261 log.debug('invalidating all repositories cache')
262 262 for repo in Repository.get_all():
263 263 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
264 264
265 265 filesystem_repos = ScmModel().repo_scan()
266 266 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
267 267 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
268 268 h.flash(_('Repositories successfully '
269 269 'rescanned added: %s ; removed: %s') %
270 270 (_repr(added), _repr(removed)),
271 271 category='success')
272 272 raise HTTPFound(h.route_path('admin_settings_mapping'))
273 273
274 274 @LoginRequired()
275 275 @HasPermissionAllDecorator('hg.admin')
276 276 @view_config(
277 277 route_name='admin_settings', request_method='GET',
278 278 renderer='rhodecode:templates/admin/settings/settings.mako')
279 279 @view_config(
280 280 route_name='admin_settings_global', request_method='GET',
281 281 renderer='rhodecode:templates/admin/settings/settings.mako')
282 282 def settings_global(self):
283 283 c = self.load_default_context()
284 284 c.active = 'global'
285 285 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 286 .get_personal_group_name_pattern()
287 287
288 288 data = render('rhodecode:templates/admin/settings/settings.mako',
289 289 self._get_template_context(c), self.request)
290 290 html = formencode.htmlfill.render(
291 291 data,
292 292 defaults=self._form_defaults(),
293 293 encoding="UTF-8",
294 294 force_defaults=False
295 295 )
296 296 return Response(html)
297 297
298 298 @LoginRequired()
299 299 @HasPermissionAllDecorator('hg.admin')
300 300 @CSRFRequired()
301 301 @view_config(
302 302 route_name='admin_settings_update', request_method='POST',
303 303 renderer='rhodecode:templates/admin/settings/settings.mako')
304 304 @view_config(
305 305 route_name='admin_settings_global_update', request_method='POST',
306 306 renderer='rhodecode:templates/admin/settings/settings.mako')
307 307 def settings_global_update(self):
308 308 _ = self.request.translate
309 309 c = self.load_default_context()
310 310 c.active = 'global'
311 311 c.personal_repo_group_default_pattern = RepoGroupModel()\
312 312 .get_personal_group_name_pattern()
313 313 application_form = ApplicationSettingsForm(self.request.translate)()
314 314 try:
315 315 form_result = application_form.to_python(dict(self.request.POST))
316 316 except formencode.Invalid as errors:
317 317 data = render('rhodecode:templates/admin/settings/settings.mako',
318 318 self._get_template_context(c), self.request)
319 319 html = formencode.htmlfill.render(
320 320 data,
321 321 defaults=errors.value,
322 322 errors=errors.error_dict or {},
323 323 prefix_error=False,
324 324 encoding="UTF-8",
325 325 force_defaults=False
326 326 )
327 327 return Response(html)
328 328
329 329 settings = [
330 330 ('title', 'rhodecode_title', 'unicode'),
331 331 ('realm', 'rhodecode_realm', 'unicode'),
332 332 ('pre_code', 'rhodecode_pre_code', 'unicode'),
333 333 ('post_code', 'rhodecode_post_code', 'unicode'),
334 334 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
335 335 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
336 336 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
337 337 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
338 338 ]
339 339 try:
340 340 for setting, form_key, type_ in settings:
341 341 sett = SettingsModel().create_or_update_setting(
342 342 setting, form_result[form_key], type_)
343 343 Session().add(sett)
344 344
345 345 Session().commit()
346 346 SettingsModel().invalidate_settings_cache()
347 347 h.flash(_('Updated application settings'), category='success')
348 348 except Exception:
349 349 log.exception("Exception while updating application settings")
350 350 h.flash(
351 351 _('Error occurred during updating application settings'),
352 352 category='error')
353 353
354 354 raise HTTPFound(h.route_path('admin_settings_global'))
355 355
356 356 @LoginRequired()
357 357 @HasPermissionAllDecorator('hg.admin')
358 358 @view_config(
359 359 route_name='admin_settings_visual', request_method='GET',
360 360 renderer='rhodecode:templates/admin/settings/settings.mako')
361 361 def settings_visual(self):
362 362 c = self.load_default_context()
363 363 c.active = 'visual'
364 364
365 365 data = render('rhodecode:templates/admin/settings/settings.mako',
366 366 self._get_template_context(c), self.request)
367 367 html = formencode.htmlfill.render(
368 368 data,
369 369 defaults=self._form_defaults(),
370 370 encoding="UTF-8",
371 371 force_defaults=False
372 372 )
373 373 return Response(html)
374 374
375 375 @LoginRequired()
376 376 @HasPermissionAllDecorator('hg.admin')
377 377 @CSRFRequired()
378 378 @view_config(
379 379 route_name='admin_settings_visual_update', request_method='POST',
380 380 renderer='rhodecode:templates/admin/settings/settings.mako')
381 381 def settings_visual_update(self):
382 382 _ = self.request.translate
383 383 c = self.load_default_context()
384 384 c.active = 'visual'
385 385 application_form = ApplicationVisualisationForm(self.request.translate)()
386 386 try:
387 387 form_result = application_form.to_python(dict(self.request.POST))
388 388 except formencode.Invalid as errors:
389 389 data = render('rhodecode:templates/admin/settings/settings.mako',
390 390 self._get_template_context(c), self.request)
391 391 html = formencode.htmlfill.render(
392 392 data,
393 393 defaults=errors.value,
394 394 errors=errors.error_dict or {},
395 395 prefix_error=False,
396 396 encoding="UTF-8",
397 397 force_defaults=False
398 398 )
399 399 return Response(html)
400 400
401 401 try:
402 402 settings = [
403 403 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
404 404 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
405 405 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
406 406 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
407 407 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
408 408 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
409 409 ('show_version', 'rhodecode_show_version', 'bool'),
410 410 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
411 411 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
412 412 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
413 413 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
414 414 ('support_url', 'rhodecode_support_url', 'unicode'),
415 415 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
416 416 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
417 417 ]
418 418 for setting, form_key, type_ in settings:
419 419 sett = SettingsModel().create_or_update_setting(
420 420 setting, form_result[form_key], type_)
421 421 Session().add(sett)
422 422
423 423 Session().commit()
424 424 SettingsModel().invalidate_settings_cache()
425 425 h.flash(_('Updated visualisation settings'), category='success')
426 426 except Exception:
427 427 log.exception("Exception updating visualization settings")
428 428 h.flash(_('Error occurred during updating '
429 429 'visualisation settings'),
430 430 category='error')
431 431
432 432 raise HTTPFound(h.route_path('admin_settings_visual'))
433 433
434 434 @LoginRequired()
435 435 @HasPermissionAllDecorator('hg.admin')
436 436 @view_config(
437 437 route_name='admin_settings_issuetracker', request_method='GET',
438 438 renderer='rhodecode:templates/admin/settings/settings.mako')
439 439 def settings_issuetracker(self):
440 440 c = self.load_default_context()
441 441 c.active = 'issuetracker'
442 442 defaults = SettingsModel().get_all_settings()
443 443
444 444 entry_key = 'rhodecode_issuetracker_pat_'
445 445
446 446 c.issuetracker_entries = {}
447 447 for k, v in defaults.items():
448 448 if k.startswith(entry_key):
449 449 uid = k[len(entry_key):]
450 450 c.issuetracker_entries[uid] = None
451 451
452 452 for uid in c.issuetracker_entries:
453 453 c.issuetracker_entries[uid] = AttributeDict({
454 454 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
455 455 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
456 456 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
457 457 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
458 458 })
459 459
460 460 return self._get_template_context(c)
461 461
462 462 @LoginRequired()
463 463 @HasPermissionAllDecorator('hg.admin')
464 464 @CSRFRequired()
465 465 @view_config(
466 466 route_name='admin_settings_issuetracker_test', request_method='POST',
467 467 renderer='string', xhr=True)
468 468 def settings_issuetracker_test(self):
469 469 return h.urlify_commit_message(
470 470 self.request.POST.get('test_text', ''),
471 471 'repo_group/test_repo1')
472 472
473 473 @LoginRequired()
474 474 @HasPermissionAllDecorator('hg.admin')
475 475 @CSRFRequired()
476 476 @view_config(
477 477 route_name='admin_settings_issuetracker_update', request_method='POST',
478 478 renderer='rhodecode:templates/admin/settings/settings.mako')
479 479 def settings_issuetracker_update(self):
480 480 _ = self.request.translate
481 481 self.load_default_context()
482 482 settings_model = IssueTrackerSettingsModel()
483 483
484 484 try:
485 485 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
486 486 except formencode.Invalid as errors:
487 487 log.exception('Failed to add new pattern')
488 488 error = errors
489 489 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
490 490 category='error')
491 491 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
492 492
493 493 if form:
494 494 for uid in form.get('delete_patterns', []):
495 495 settings_model.delete_entries(uid)
496 496
497 497 for pattern in form.get('patterns', []):
498 498 for setting, value, type_ in pattern:
499 499 sett = settings_model.create_or_update_setting(
500 500 setting, value, type_)
501 501 Session().add(sett)
502 502
503 503 Session().commit()
504 504
505 505 SettingsModel().invalidate_settings_cache()
506 506 h.flash(_('Updated issue tracker entries'), category='success')
507 507 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
508 508
509 509 @LoginRequired()
510 510 @HasPermissionAllDecorator('hg.admin')
511 511 @CSRFRequired()
512 512 @view_config(
513 513 route_name='admin_settings_issuetracker_delete', request_method='POST',
514 514 renderer='rhodecode:templates/admin/settings/settings.mako')
515 515 def settings_issuetracker_delete(self):
516 516 _ = self.request.translate
517 517 self.load_default_context()
518 518 uid = self.request.POST.get('uid')
519 519 try:
520 520 IssueTrackerSettingsModel().delete_entries(uid)
521 521 except Exception:
522 522 log.exception('Failed to delete issue tracker setting %s', uid)
523 523 raise HTTPNotFound()
524 524 h.flash(_('Removed issue tracker entry'), category='success')
525 525 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
526 526
527 527 @LoginRequired()
528 528 @HasPermissionAllDecorator('hg.admin')
529 529 @view_config(
530 530 route_name='admin_settings_email', request_method='GET',
531 531 renderer='rhodecode:templates/admin/settings/settings.mako')
532 532 def settings_email(self):
533 533 c = self.load_default_context()
534 534 c.active = 'email'
535 535 c.rhodecode_ini = rhodecode.CONFIG
536 536
537 537 data = render('rhodecode:templates/admin/settings/settings.mako',
538 538 self._get_template_context(c), self.request)
539 539 html = formencode.htmlfill.render(
540 540 data,
541 541 defaults=self._form_defaults(),
542 542 encoding="UTF-8",
543 543 force_defaults=False
544 544 )
545 545 return Response(html)
546 546
547 547 @LoginRequired()
548 548 @HasPermissionAllDecorator('hg.admin')
549 549 @CSRFRequired()
550 550 @view_config(
551 551 route_name='admin_settings_email_update', request_method='POST',
552 552 renderer='rhodecode:templates/admin/settings/settings.mako')
553 553 def settings_email_update(self):
554 554 _ = self.request.translate
555 555 c = self.load_default_context()
556 556 c.active = 'email'
557 557
558 558 test_email = self.request.POST.get('test_email')
559 559
560 560 if not test_email:
561 561 h.flash(_('Please enter email address'), category='error')
562 562 raise HTTPFound(h.route_path('admin_settings_email'))
563 563
564 564 email_kwargs = {
565 565 'date': datetime.datetime.now(),
566 566 'user': c.rhodecode_user,
567 567 'rhodecode_version': c.rhodecode_version
568 568 }
569 569
570 570 (subject, headers, email_body,
571 571 email_body_plaintext) = EmailNotificationModel().render_email(
572 572 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
573 573
574 574 recipients = [test_email] if test_email else None
575 575
576 576 run_task(tasks.send_email, recipients, subject,
577 577 email_body_plaintext, email_body)
578 578
579 579 h.flash(_('Send email task created'), category='success')
580 580 raise HTTPFound(h.route_path('admin_settings_email'))
581 581
582 582 @LoginRequired()
583 583 @HasPermissionAllDecorator('hg.admin')
584 584 @view_config(
585 585 route_name='admin_settings_hooks', request_method='GET',
586 586 renderer='rhodecode:templates/admin/settings/settings.mako')
587 587 def settings_hooks(self):
588 588 c = self.load_default_context()
589 589 c.active = 'hooks'
590 590
591 591 model = SettingsModel()
592 592 c.hooks = model.get_builtin_hooks()
593 593 c.custom_hooks = model.get_custom_hooks()
594 594
595 595 data = render('rhodecode:templates/admin/settings/settings.mako',
596 596 self._get_template_context(c), self.request)
597 597 html = formencode.htmlfill.render(
598 598 data,
599 599 defaults=self._form_defaults(),
600 600 encoding="UTF-8",
601 601 force_defaults=False
602 602 )
603 603 return Response(html)
604 604
605 605 @LoginRequired()
606 606 @HasPermissionAllDecorator('hg.admin')
607 607 @CSRFRequired()
608 608 @view_config(
609 609 route_name='admin_settings_hooks_update', request_method='POST',
610 610 renderer='rhodecode:templates/admin/settings/settings.mako')
611 611 @view_config(
612 612 route_name='admin_settings_hooks_delete', request_method='POST',
613 613 renderer='rhodecode:templates/admin/settings/settings.mako')
614 614 def settings_hooks_update(self):
615 615 _ = self.request.translate
616 616 c = self.load_default_context()
617 617 c.active = 'hooks'
618 618 if c.visual.allow_custom_hooks_settings:
619 619 ui_key = self.request.POST.get('new_hook_ui_key')
620 620 ui_value = self.request.POST.get('new_hook_ui_value')
621 621
622 622 hook_id = self.request.POST.get('hook_id')
623 623 new_hook = False
624 624
625 625 model = SettingsModel()
626 626 try:
627 627 if ui_value and ui_key:
628 628 model.create_or_update_hook(ui_key, ui_value)
629 629 h.flash(_('Added new hook'), category='success')
630 630 new_hook = True
631 631 elif hook_id:
632 632 RhodeCodeUi.delete(hook_id)
633 633 Session().commit()
634 634
635 635 # check for edits
636 636 update = False
637 637 _d = self.request.POST.dict_of_lists()
638 638 for k, v in zip(_d.get('hook_ui_key', []),
639 639 _d.get('hook_ui_value_new', [])):
640 640 model.create_or_update_hook(k, v)
641 641 update = True
642 642
643 643 if update and not new_hook:
644 644 h.flash(_('Updated hooks'), category='success')
645 645 Session().commit()
646 646 except Exception:
647 647 log.exception("Exception during hook creation")
648 648 h.flash(_('Error occurred during hook creation'),
649 649 category='error')
650 650
651 651 raise HTTPFound(h.route_path('admin_settings_hooks'))
652 652
653 653 @LoginRequired()
654 654 @HasPermissionAllDecorator('hg.admin')
655 655 @view_config(
656 656 route_name='admin_settings_search', request_method='GET',
657 657 renderer='rhodecode:templates/admin/settings/settings.mako')
658 658 def settings_search(self):
659 659 c = self.load_default_context()
660 660 c.active = 'search'
661 661
662 662 searcher = searcher_from_config(self.request.registry.settings)
663 c.statistics = searcher.statistics()
663 c.statistics = searcher.statistics(self.request.translate)
664 664
665 665 return self._get_template_context(c)
666 666
667 667 @LoginRequired()
668 668 @HasPermissionAllDecorator('hg.admin')
669 669 @view_config(
670 670 route_name='admin_settings_labs', request_method='GET',
671 671 renderer='rhodecode:templates/admin/settings/settings.mako')
672 672 def settings_labs(self):
673 673 c = self.load_default_context()
674 674 if not c.labs_active:
675 675 raise HTTPFound(h.route_path('admin_settings'))
676 676
677 677 c.active = 'labs'
678 678 c.lab_settings = _LAB_SETTINGS
679 679
680 680 data = render('rhodecode:templates/admin/settings/settings.mako',
681 681 self._get_template_context(c), self.request)
682 682 html = formencode.htmlfill.render(
683 683 data,
684 684 defaults=self._form_defaults(),
685 685 encoding="UTF-8",
686 686 force_defaults=False
687 687 )
688 688 return Response(html)
689 689
690 690 @LoginRequired()
691 691 @HasPermissionAllDecorator('hg.admin')
692 692 @CSRFRequired()
693 693 @view_config(
694 694 route_name='admin_settings_labs_update', request_method='POST',
695 695 renderer='rhodecode:templates/admin/settings/settings.mako')
696 696 def settings_labs_update(self):
697 697 _ = self.request.translate
698 698 c = self.load_default_context()
699 699 c.active = 'labs'
700 700
701 701 application_form = LabsSettingsForm(self.request.translate)()
702 702 try:
703 703 form_result = application_form.to_python(dict(self.request.POST))
704 704 except formencode.Invalid as errors:
705 705 h.flash(
706 706 _('Some form inputs contain invalid data.'),
707 707 category='error')
708 708 data = render('rhodecode:templates/admin/settings/settings.mako',
709 709 self._get_template_context(c), self.request)
710 710 html = formencode.htmlfill.render(
711 711 data,
712 712 defaults=errors.value,
713 713 errors=errors.error_dict or {},
714 714 prefix_error=False,
715 715 encoding="UTF-8",
716 716 force_defaults=False
717 717 )
718 718 return Response(html)
719 719
720 720 try:
721 721 session = Session()
722 722 for setting in _LAB_SETTINGS:
723 723 setting_name = setting.key[len('rhodecode_'):]
724 724 sett = SettingsModel().create_or_update_setting(
725 725 setting_name, form_result[setting.key], setting.type)
726 726 session.add(sett)
727 727
728 728 except Exception:
729 729 log.exception('Exception while updating lab settings')
730 730 h.flash(_('Error occurred during updating labs settings'),
731 731 category='error')
732 732 else:
733 733 Session().commit()
734 734 SettingsModel().invalidate_settings_cache()
735 735 h.flash(_('Updated Labs settings'), category='success')
736 736 raise HTTPFound(h.route_path('admin_settings_labs'))
737 737
738 738 data = render('rhodecode:templates/admin/settings/settings.mako',
739 739 self._get_template_context(c), self.request)
740 740 html = formencode.htmlfill.render(
741 741 data,
742 742 defaults=self._form_defaults(),
743 743 encoding="UTF-8",
744 744 force_defaults=False
745 745 )
746 746 return Response(html)
747 747
748 748
749 749 # :param key: name of the setting including the 'rhodecode_' prefix
750 750 # :param type: the RhodeCodeSetting type to use.
751 751 # :param group: the i18ned group in which we should dispaly this setting
752 752 # :param label: the i18ned label we should display for this setting
753 753 # :param help: the i18ned help we should dispaly for this setting
754 754 LabSetting = collections.namedtuple(
755 755 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
756 756
757 757
758 758 # This list has to be kept in sync with the form
759 759 # rhodecode.model.forms.LabsSettingsForm.
760 760 _LAB_SETTINGS = [
761 761
762 762 ]
@@ -1,1190 +1,1189 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34 from rhodecode.events import trigger
35 35
36 36 from rhodecode.lib import audit_logger
37 37 from rhodecode.lib.exceptions import (
38 38 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
39 39 UserOwnsUserGroupsException, DefaultUserException)
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.auth import (
42 42 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
43 43 from rhodecode.lib import helpers as h
44 44 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
45 45 from rhodecode.model.auth_token import AuthTokenModel
46 46 from rhodecode.model.forms import (
47 47 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
48 48 UserExtraEmailForm, UserExtraIpForm)
49 49 from rhodecode.model.permission import PermissionModel
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51 from rhodecode.model.ssh_key import SshKeyModel
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.user_group import UserGroupModel
54 54 from rhodecode.model.db import (
55 55 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
56 56 UserApiKeys, UserSshKeys, RepoGroup)
57 57 from rhodecode.model.meta import Session
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class AdminUsersView(BaseAppView, DataGridAppView):
63 63
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context()
66 66 return c
67 67
68 68 @LoginRequired()
69 69 @HasPermissionAllDecorator('hg.admin')
70 70 @view_config(
71 71 route_name='users', request_method='GET',
72 72 renderer='rhodecode:templates/admin/users/users.mako')
73 73 def users_list(self):
74 74 c = self.load_default_context()
75 75 return self._get_template_context(c)
76 76
77 77 @LoginRequired()
78 78 @HasPermissionAllDecorator('hg.admin')
79 79 @view_config(
80 80 # renderer defined below
81 81 route_name='users_data', request_method='GET',
82 82 renderer='json_ext', xhr=True)
83 83 def users_list_data(self):
84 84 self.load_default_context()
85 85 column_map = {
86 86 'first_name': 'name',
87 87 'last_name': 'lastname',
88 88 }
89 89 draw, start, limit = self._extract_chunk(self.request)
90 90 search_q, order_by, order_dir = self._extract_ordering(
91 91 self.request, column_map=column_map)
92 92
93 93 _render = self.request.get_partial_renderer(
94 94 'rhodecode:templates/data_table/_dt_elements.mako')
95 95
96 96 def user_actions(user_id, username):
97 97 return _render("user_actions", user_id, username)
98 98
99 99 users_data_total_count = User.query()\
100 100 .filter(User.username != User.DEFAULT_USER) \
101 101 .count()
102 102
103 103 # json generate
104 104 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105 105
106 106 if search_q:
107 107 like_expression = u'%{}%'.format(safe_unicode(search_q))
108 108 base_q = base_q.filter(or_(
109 109 User.username.ilike(like_expression),
110 110 User._email.ilike(like_expression),
111 111 User.name.ilike(like_expression),
112 112 User.lastname.ilike(like_expression),
113 113 ))
114 114
115 115 users_data_total_filtered_count = base_q.count()
116 116
117 117 sort_col = getattr(User, order_by, None)
118 118 if sort_col:
119 119 if order_dir == 'asc':
120 120 # handle null values properly to order by NULL last
121 121 if order_by in ['last_activity']:
122 122 sort_col = coalesce(sort_col, datetime.date.max)
123 123 sort_col = sort_col.asc()
124 124 else:
125 125 # handle null values properly to order by NULL last
126 126 if order_by in ['last_activity']:
127 127 sort_col = coalesce(sort_col, datetime.date.min)
128 128 sort_col = sort_col.desc()
129 129
130 130 base_q = base_q.order_by(sort_col)
131 131 base_q = base_q.offset(start).limit(limit)
132 132
133 133 users_list = base_q.all()
134 134
135 135 users_data = []
136 136 for user in users_list:
137 137 users_data.append({
138 138 "username": h.gravatar_with_user(self.request, user.username),
139 139 "email": user.email,
140 140 "first_name": user.first_name,
141 141 "last_name": user.last_name,
142 142 "last_login": h.format_date(user.last_login),
143 143 "last_activity": h.format_date(user.last_activity),
144 144 "active": h.bool2icon(user.active),
145 145 "active_raw": user.active,
146 146 "admin": h.bool2icon(user.admin),
147 147 "extern_type": user.extern_type,
148 148 "extern_name": user.extern_name,
149 149 "action": user_actions(user.user_id, user.username),
150 150 })
151 151
152 152 data = ({
153 153 'draw': draw,
154 154 'data': users_data,
155 155 'recordsTotal': users_data_total_count,
156 156 'recordsFiltered': users_data_total_filtered_count,
157 157 })
158 158
159 159 return data
160 160
161 161 def _set_personal_repo_group_template_vars(self, c_obj):
162 162 DummyUser = AttributeDict({
163 163 'username': '${username}',
164 164 'user_id': '${user_id}',
165 165 })
166 166 c_obj.default_create_repo_group = RepoGroupModel() \
167 167 .get_default_create_personal_repo_group()
168 168 c_obj.personal_repo_group_name = RepoGroupModel() \
169 169 .get_personal_group_name(DummyUser)
170 170
171 171 @LoginRequired()
172 172 @HasPermissionAllDecorator('hg.admin')
173 173 @view_config(
174 174 route_name='users_new', request_method='GET',
175 175 renderer='rhodecode:templates/admin/users/user_add.mako')
176 176 def users_new(self):
177 177 _ = self.request.translate
178 178 c = self.load_default_context()
179 179 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
180 180 self._set_personal_repo_group_template_vars(c)
181 181 return self._get_template_context(c)
182 182
183 183 @LoginRequired()
184 184 @HasPermissionAllDecorator('hg.admin')
185 185 @CSRFRequired()
186 186 @view_config(
187 187 route_name='users_create', request_method='POST',
188 188 renderer='rhodecode:templates/admin/users/user_add.mako')
189 189 def users_create(self):
190 190 _ = self.request.translate
191 191 c = self.load_default_context()
192 192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
193 193 user_model = UserModel()
194 194 user_form = UserForm(self.request.translate)()
195 195 try:
196 196 form_result = user_form.to_python(dict(self.request.POST))
197 197 user = user_model.create(form_result)
198 198 Session().flush()
199 199 creation_data = user.get_api_data()
200 200 username = form_result['username']
201 201
202 202 audit_logger.store_web(
203 203 'user.create', action_data={'data': creation_data},
204 204 user=c.rhodecode_user)
205 205
206 206 user_link = h.link_to(
207 207 h.escape(username),
208 208 h.route_path('user_edit', user_id=user.user_id))
209 209 h.flash(h.literal(_('Created user %(user_link)s')
210 210 % {'user_link': user_link}), category='success')
211 211 Session().commit()
212 212 except formencode.Invalid as errors:
213 213 self._set_personal_repo_group_template_vars(c)
214 214 data = render(
215 215 'rhodecode:templates/admin/users/user_add.mako',
216 216 self._get_template_context(c), self.request)
217 217 html = formencode.htmlfill.render(
218 218 data,
219 219 defaults=errors.value,
220 220 errors=errors.error_dict or {},
221 221 prefix_error=False,
222 222 encoding="UTF-8",
223 223 force_defaults=False
224 224 )
225 225 return Response(html)
226 226 except UserCreationError as e:
227 227 h.flash(e, 'error')
228 228 except Exception:
229 229 log.exception("Exception creation of user")
230 230 h.flash(_('Error occurred during creation of user %s')
231 231 % self.request.POST.get('username'), category='error')
232 232 raise HTTPFound(h.route_path('users'))
233 233
234 234
235 235 class UsersView(UserAppView):
236 236 ALLOW_SCOPED_TOKENS = False
237 237 """
238 238 This view has alternative version inside EE, if modified please take a look
239 239 in there as well.
240 240 """
241 241
242 242 def load_default_context(self):
243 243 c = self._get_local_tmpl_context()
244 244 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
245 245 c.allowed_languages = [
246 246 ('en', 'English (en)'),
247 247 ('de', 'German (de)'),
248 248 ('fr', 'French (fr)'),
249 249 ('it', 'Italian (it)'),
250 250 ('ja', 'Japanese (ja)'),
251 251 ('pl', 'Polish (pl)'),
252 252 ('pt', 'Portuguese (pt)'),
253 253 ('ru', 'Russian (ru)'),
254 254 ('zh', 'Chinese (zh)'),
255 255 ]
256 256 req = self.request
257 257
258 258 c.available_permissions = req.registry.settings['available_permissions']
259 259 PermissionModel().set_global_permission_choices(
260 260 c, gettext_translator=req.translate)
261 261
262
263 262 return c
264 263
265 264 @LoginRequired()
266 265 @HasPermissionAllDecorator('hg.admin')
267 266 @CSRFRequired()
268 267 @view_config(
269 268 route_name='user_update', request_method='POST',
270 269 renderer='rhodecode:templates/admin/users/user_edit.mako')
271 270 def user_update(self):
272 271 _ = self.request.translate
273 272 c = self.load_default_context()
274 273
275 274 user_id = self.db_user_id
276 275 c.user = self.db_user
277 276
278 277 c.active = 'profile'
279 278 c.extern_type = c.user.extern_type
280 279 c.extern_name = c.user.extern_name
281 280 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
282 281 available_languages = [x[0] for x in c.allowed_languages]
283 282 _form = UserForm(self.request.translate, edit=True,
284 283 available_languages=available_languages,
285 284 old_data={'user_id': user_id,
286 285 'email': c.user.email})()
287 286 form_result = {}
288 287 old_values = c.user.get_api_data()
289 288 try:
290 289 form_result = _form.to_python(dict(self.request.POST))
291 290 skip_attrs = ['extern_type', 'extern_name']
292 291 # TODO: plugin should define if username can be updated
293 292 if c.extern_type != "rhodecode":
294 293 # forbid updating username for external accounts
295 294 skip_attrs.append('username')
296 295
297 296 UserModel().update_user(
298 297 user_id, skip_attrs=skip_attrs, **form_result)
299 298
300 299 audit_logger.store_web(
301 300 'user.edit', action_data={'old_data': old_values},
302 301 user=c.rhodecode_user)
303 302
304 303 Session().commit()
305 304 h.flash(_('User updated successfully'), category='success')
306 305 except formencode.Invalid as errors:
307 306 data = render(
308 307 'rhodecode:templates/admin/users/user_edit.mako',
309 308 self._get_template_context(c), self.request)
310 309 html = formencode.htmlfill.render(
311 310 data,
312 311 defaults=errors.value,
313 312 errors=errors.error_dict or {},
314 313 prefix_error=False,
315 314 encoding="UTF-8",
316 315 force_defaults=False
317 316 )
318 317 return Response(html)
319 318 except UserCreationError as e:
320 319 h.flash(e, 'error')
321 320 except Exception:
322 321 log.exception("Exception updating user")
323 322 h.flash(_('Error occurred during update of user %s')
324 323 % form_result.get('username'), category='error')
325 324 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
326 325
327 326 @LoginRequired()
328 327 @HasPermissionAllDecorator('hg.admin')
329 328 @CSRFRequired()
330 329 @view_config(
331 330 route_name='user_delete', request_method='POST',
332 331 renderer='rhodecode:templates/admin/users/user_edit.mako')
333 332 def user_delete(self):
334 333 _ = self.request.translate
335 334 c = self.load_default_context()
336 335 c.user = self.db_user
337 336
338 337 _repos = c.user.repositories
339 338 _repo_groups = c.user.repository_groups
340 339 _user_groups = c.user.user_groups
341 340
342 341 handle_repos = None
343 342 handle_repo_groups = None
344 343 handle_user_groups = None
345 344 # dummy call for flash of handle
346 345 set_handle_flash_repos = lambda: None
347 346 set_handle_flash_repo_groups = lambda: None
348 347 set_handle_flash_user_groups = lambda: None
349 348
350 349 if _repos and self.request.POST.get('user_repos'):
351 350 do = self.request.POST['user_repos']
352 351 if do == 'detach':
353 352 handle_repos = 'detach'
354 353 set_handle_flash_repos = lambda: h.flash(
355 354 _('Detached %s repositories') % len(_repos),
356 355 category='success')
357 356 elif do == 'delete':
358 357 handle_repos = 'delete'
359 358 set_handle_flash_repos = lambda: h.flash(
360 359 _('Deleted %s repositories') % len(_repos),
361 360 category='success')
362 361
363 362 if _repo_groups and self.request.POST.get('user_repo_groups'):
364 363 do = self.request.POST['user_repo_groups']
365 364 if do == 'detach':
366 365 handle_repo_groups = 'detach'
367 366 set_handle_flash_repo_groups = lambda: h.flash(
368 367 _('Detached %s repository groups') % len(_repo_groups),
369 368 category='success')
370 369 elif do == 'delete':
371 370 handle_repo_groups = 'delete'
372 371 set_handle_flash_repo_groups = lambda: h.flash(
373 372 _('Deleted %s repository groups') % len(_repo_groups),
374 373 category='success')
375 374
376 375 if _user_groups and self.request.POST.get('user_user_groups'):
377 376 do = self.request.POST['user_user_groups']
378 377 if do == 'detach':
379 378 handle_user_groups = 'detach'
380 379 set_handle_flash_user_groups = lambda: h.flash(
381 380 _('Detached %s user groups') % len(_user_groups),
382 381 category='success')
383 382 elif do == 'delete':
384 383 handle_user_groups = 'delete'
385 384 set_handle_flash_user_groups = lambda: h.flash(
386 385 _('Deleted %s user groups') % len(_user_groups),
387 386 category='success')
388 387
389 388 old_values = c.user.get_api_data()
390 389 try:
391 390 UserModel().delete(c.user, handle_repos=handle_repos,
392 391 handle_repo_groups=handle_repo_groups,
393 392 handle_user_groups=handle_user_groups)
394 393
395 394 audit_logger.store_web(
396 395 'user.delete', action_data={'old_data': old_values},
397 396 user=c.rhodecode_user)
398 397
399 398 Session().commit()
400 399 set_handle_flash_repos()
401 400 set_handle_flash_repo_groups()
402 401 set_handle_flash_user_groups()
403 402 h.flash(_('Successfully deleted user'), category='success')
404 403 except (UserOwnsReposException, UserOwnsRepoGroupsException,
405 404 UserOwnsUserGroupsException, DefaultUserException) as e:
406 405 h.flash(e, category='warning')
407 406 except Exception:
408 407 log.exception("Exception during deletion of user")
409 408 h.flash(_('An error occurred during deletion of user'),
410 409 category='error')
411 410 raise HTTPFound(h.route_path('users'))
412 411
413 412 @LoginRequired()
414 413 @HasPermissionAllDecorator('hg.admin')
415 414 @view_config(
416 415 route_name='user_edit', request_method='GET',
417 416 renderer='rhodecode:templates/admin/users/user_edit.mako')
418 417 def user_edit(self):
419 418 _ = self.request.translate
420 419 c = self.load_default_context()
421 420 c.user = self.db_user
422 421
423 422 c.active = 'profile'
424 423 c.extern_type = c.user.extern_type
425 424 c.extern_name = c.user.extern_name
426 425 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
427 426
428 427 defaults = c.user.get_dict()
429 428 defaults.update({'language': c.user.user_data.get('language')})
430 429
431 430 data = render(
432 431 'rhodecode:templates/admin/users/user_edit.mako',
433 432 self._get_template_context(c), self.request)
434 433 html = formencode.htmlfill.render(
435 434 data,
436 435 defaults=defaults,
437 436 encoding="UTF-8",
438 437 force_defaults=False
439 438 )
440 439 return Response(html)
441 440
442 441 @LoginRequired()
443 442 @HasPermissionAllDecorator('hg.admin')
444 443 @view_config(
445 444 route_name='user_edit_advanced', request_method='GET',
446 445 renderer='rhodecode:templates/admin/users/user_edit.mako')
447 446 def user_edit_advanced(self):
448 447 _ = self.request.translate
449 448 c = self.load_default_context()
450 449
451 450 user_id = self.db_user_id
452 451 c.user = self.db_user
453 452
454 453 c.active = 'advanced'
455 454 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
456 455 c.personal_repo_group_name = RepoGroupModel()\
457 456 .get_personal_group_name(c.user)
458 457
459 458 c.user_to_review_rules = sorted(
460 459 (x.user for x in c.user.user_review_rules),
461 460 key=lambda u: u.username.lower())
462 461
463 462 c.first_admin = User.get_first_super_admin()
464 463 defaults = c.user.get_dict()
465 464
466 465 # Interim workaround if the user participated on any pull requests as a
467 466 # reviewer.
468 467 has_review = len(c.user.reviewer_pull_requests)
469 468 c.can_delete_user = not has_review
470 469 c.can_delete_user_message = ''
471 470 inactive_link = h.link_to(
472 471 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
473 472 if has_review == 1:
474 473 c.can_delete_user_message = h.literal(_(
475 474 'The user participates as reviewer in {} pull request and '
476 475 'cannot be deleted. \nYou can set the user to '
477 476 '"{}" instead of deleting it.').format(
478 477 has_review, inactive_link))
479 478 elif has_review:
480 479 c.can_delete_user_message = h.literal(_(
481 480 'The user participates as reviewer in {} pull requests and '
482 481 'cannot be deleted. \nYou can set the user to '
483 482 '"{}" instead of deleting it.').format(
484 483 has_review, inactive_link))
485 484
486 485 data = render(
487 486 'rhodecode:templates/admin/users/user_edit.mako',
488 487 self._get_template_context(c), self.request)
489 488 html = formencode.htmlfill.render(
490 489 data,
491 490 defaults=defaults,
492 491 encoding="UTF-8",
493 492 force_defaults=False
494 493 )
495 494 return Response(html)
496 495
497 496 @LoginRequired()
498 497 @HasPermissionAllDecorator('hg.admin')
499 498 @view_config(
500 499 route_name='user_edit_global_perms', request_method='GET',
501 500 renderer='rhodecode:templates/admin/users/user_edit.mako')
502 501 def user_edit_global_perms(self):
503 502 _ = self.request.translate
504 503 c = self.load_default_context()
505 504 c.user = self.db_user
506 505
507 506 c.active = 'global_perms'
508 507
509 508 c.default_user = User.get_default_user()
510 509 defaults = c.user.get_dict()
511 510 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
512 511 defaults.update(c.default_user.get_default_perms())
513 512 defaults.update(c.user.get_default_perms())
514 513
515 514 data = render(
516 515 'rhodecode:templates/admin/users/user_edit.mako',
517 516 self._get_template_context(c), self.request)
518 517 html = formencode.htmlfill.render(
519 518 data,
520 519 defaults=defaults,
521 520 encoding="UTF-8",
522 521 force_defaults=False
523 522 )
524 523 return Response(html)
525 524
526 525 @LoginRequired()
527 526 @HasPermissionAllDecorator('hg.admin')
528 527 @CSRFRequired()
529 528 @view_config(
530 529 route_name='user_edit_global_perms_update', request_method='POST',
531 530 renderer='rhodecode:templates/admin/users/user_edit.mako')
532 531 def user_edit_global_perms_update(self):
533 532 _ = self.request.translate
534 533 c = self.load_default_context()
535 534
536 535 user_id = self.db_user_id
537 536 c.user = self.db_user
538 537
539 538 c.active = 'global_perms'
540 539 try:
541 540 # first stage that verifies the checkbox
542 541 _form = UserIndividualPermissionsForm(self.request.translate)
543 542 form_result = _form.to_python(dict(self.request.POST))
544 543 inherit_perms = form_result['inherit_default_permissions']
545 544 c.user.inherit_default_permissions = inherit_perms
546 545 Session().add(c.user)
547 546
548 547 if not inherit_perms:
549 548 # only update the individual ones if we un check the flag
550 549 _form = UserPermissionsForm(
551 550 self.request.translate,
552 551 [x[0] for x in c.repo_create_choices],
553 552 [x[0] for x in c.repo_create_on_write_choices],
554 553 [x[0] for x in c.repo_group_create_choices],
555 554 [x[0] for x in c.user_group_create_choices],
556 555 [x[0] for x in c.fork_choices],
557 556 [x[0] for x in c.inherit_default_permission_choices])()
558 557
559 558 form_result = _form.to_python(dict(self.request.POST))
560 559 form_result.update({'perm_user_id': c.user.user_id})
561 560
562 561 PermissionModel().update_user_permissions(form_result)
563 562
564 563 # TODO(marcink): implement global permissions
565 564 # audit_log.store_web('user.edit.permissions')
566 565
567 566 Session().commit()
568 567 h.flash(_('User global permissions updated successfully'),
569 568 category='success')
570 569
571 570 except formencode.Invalid as errors:
572 571 data = render(
573 572 'rhodecode:templates/admin/users/user_edit.mako',
574 573 self._get_template_context(c), self.request)
575 574 html = formencode.htmlfill.render(
576 575 data,
577 576 defaults=errors.value,
578 577 errors=errors.error_dict or {},
579 578 prefix_error=False,
580 579 encoding="UTF-8",
581 580 force_defaults=False
582 581 )
583 582 return Response(html)
584 583 except Exception:
585 584 log.exception("Exception during permissions saving")
586 585 h.flash(_('An error occurred during permissions saving'),
587 586 category='error')
588 587 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
589 588
590 589 @LoginRequired()
591 590 @HasPermissionAllDecorator('hg.admin')
592 591 @CSRFRequired()
593 592 @view_config(
594 593 route_name='user_force_password_reset', request_method='POST',
595 594 renderer='rhodecode:templates/admin/users/user_edit.mako')
596 595 def user_force_password_reset(self):
597 596 """
598 597 toggle reset password flag for this user
599 598 """
600 599 _ = self.request.translate
601 600 c = self.load_default_context()
602 601
603 602 user_id = self.db_user_id
604 603 c.user = self.db_user
605 604
606 605 try:
607 606 old_value = c.user.user_data.get('force_password_change')
608 607 c.user.update_userdata(force_password_change=not old_value)
609 608
610 609 if old_value:
611 610 msg = _('Force password change disabled for user')
612 611 audit_logger.store_web(
613 612 'user.edit.password_reset.disabled',
614 613 user=c.rhodecode_user)
615 614 else:
616 615 msg = _('Force password change enabled for user')
617 616 audit_logger.store_web(
618 617 'user.edit.password_reset.enabled',
619 618 user=c.rhodecode_user)
620 619
621 620 Session().commit()
622 621 h.flash(msg, category='success')
623 622 except Exception:
624 623 log.exception("Exception during password reset for user")
625 624 h.flash(_('An error occurred during password reset for user'),
626 625 category='error')
627 626
628 627 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
629 628
630 629 @LoginRequired()
631 630 @HasPermissionAllDecorator('hg.admin')
632 631 @CSRFRequired()
633 632 @view_config(
634 633 route_name='user_create_personal_repo_group', request_method='POST',
635 634 renderer='rhodecode:templates/admin/users/user_edit.mako')
636 635 def user_create_personal_repo_group(self):
637 636 """
638 637 Create personal repository group for this user
639 638 """
640 639 from rhodecode.model.repo_group import RepoGroupModel
641 640
642 641 _ = self.request.translate
643 642 c = self.load_default_context()
644 643
645 644 user_id = self.db_user_id
646 645 c.user = self.db_user
647 646
648 647 personal_repo_group = RepoGroup.get_user_personal_repo_group(
649 648 c.user.user_id)
650 649 if personal_repo_group:
651 650 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
652 651
653 652 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
654 653 c.user)
655 654 named_personal_group = RepoGroup.get_by_group_name(
656 655 personal_repo_group_name)
657 656 try:
658 657
659 658 if named_personal_group and named_personal_group.user_id == c.user.user_id:
660 659 # migrate the same named group, and mark it as personal
661 660 named_personal_group.personal = True
662 661 Session().add(named_personal_group)
663 662 Session().commit()
664 663 msg = _('Linked repository group `%s` as personal' % (
665 664 personal_repo_group_name,))
666 665 h.flash(msg, category='success')
667 666 elif not named_personal_group:
668 667 RepoGroupModel().create_personal_repo_group(c.user)
669 668
670 669 msg = _('Created repository group `%s`' % (
671 670 personal_repo_group_name,))
672 671 h.flash(msg, category='success')
673 672 else:
674 673 msg = _('Repository group `%s` is already taken' % (
675 674 personal_repo_group_name,))
676 675 h.flash(msg, category='warning')
677 676 except Exception:
678 677 log.exception("Exception during repository group creation")
679 678 msg = _(
680 679 'An error occurred during repository group creation for user')
681 680 h.flash(msg, category='error')
682 681 Session().rollback()
683 682
684 683 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
685 684
686 685 @LoginRequired()
687 686 @HasPermissionAllDecorator('hg.admin')
688 687 @view_config(
689 688 route_name='edit_user_auth_tokens', request_method='GET',
690 689 renderer='rhodecode:templates/admin/users/user_edit.mako')
691 690 def auth_tokens(self):
692 691 _ = self.request.translate
693 692 c = self.load_default_context()
694 693 c.user = self.db_user
695 694
696 695 c.active = 'auth_tokens'
697 696
698 697 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
699 698 c.role_values = [
700 699 (x, AuthTokenModel.cls._get_role_name(x))
701 700 for x in AuthTokenModel.cls.ROLES]
702 701 c.role_options = [(c.role_values, _("Role"))]
703 702 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
704 703 c.user.user_id, show_expired=True)
705 704 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
706 705 return self._get_template_context(c)
707 706
708 707 def maybe_attach_token_scope(self, token):
709 708 # implemented in EE edition
710 709 pass
711 710
712 711 @LoginRequired()
713 712 @HasPermissionAllDecorator('hg.admin')
714 713 @CSRFRequired()
715 714 @view_config(
716 715 route_name='edit_user_auth_tokens_add', request_method='POST')
717 716 def auth_tokens_add(self):
718 717 _ = self.request.translate
719 718 c = self.load_default_context()
720 719
721 720 user_id = self.db_user_id
722 721 c.user = self.db_user
723 722
724 723 user_data = c.user.get_api_data()
725 724 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
726 725 description = self.request.POST.get('description')
727 726 role = self.request.POST.get('role')
728 727
729 728 token = AuthTokenModel().create(
730 729 c.user.user_id, description, lifetime, role)
731 730 token_data = token.get_api_data()
732 731
733 732 self.maybe_attach_token_scope(token)
734 733 audit_logger.store_web(
735 734 'user.edit.token.add', action_data={
736 735 'data': {'token': token_data, 'user': user_data}},
737 736 user=self._rhodecode_user, )
738 737 Session().commit()
739 738
740 739 h.flash(_("Auth token successfully created"), category='success')
741 740 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
742 741
743 742 @LoginRequired()
744 743 @HasPermissionAllDecorator('hg.admin')
745 744 @CSRFRequired()
746 745 @view_config(
747 746 route_name='edit_user_auth_tokens_delete', request_method='POST')
748 747 def auth_tokens_delete(self):
749 748 _ = self.request.translate
750 749 c = self.load_default_context()
751 750
752 751 user_id = self.db_user_id
753 752 c.user = self.db_user
754 753
755 754 user_data = c.user.get_api_data()
756 755
757 756 del_auth_token = self.request.POST.get('del_auth_token')
758 757
759 758 if del_auth_token:
760 759 token = UserApiKeys.get_or_404(del_auth_token)
761 760 token_data = token.get_api_data()
762 761
763 762 AuthTokenModel().delete(del_auth_token, c.user.user_id)
764 763 audit_logger.store_web(
765 764 'user.edit.token.delete', action_data={
766 765 'data': {'token': token_data, 'user': user_data}},
767 766 user=self._rhodecode_user,)
768 767 Session().commit()
769 768 h.flash(_("Auth token successfully deleted"), category='success')
770 769
771 770 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
772 771
773 772 @LoginRequired()
774 773 @HasPermissionAllDecorator('hg.admin')
775 774 @view_config(
776 775 route_name='edit_user_ssh_keys', request_method='GET',
777 776 renderer='rhodecode:templates/admin/users/user_edit.mako')
778 777 def ssh_keys(self):
779 778 _ = self.request.translate
780 779 c = self.load_default_context()
781 780 c.user = self.db_user
782 781
783 782 c.active = 'ssh_keys'
784 783 c.default_key = self.request.GET.get('default_key')
785 784 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
786 785 return self._get_template_context(c)
787 786
788 787 @LoginRequired()
789 788 @HasPermissionAllDecorator('hg.admin')
790 789 @view_config(
791 790 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
792 791 renderer='rhodecode:templates/admin/users/user_edit.mako')
793 792 def ssh_keys_generate_keypair(self):
794 793 _ = self.request.translate
795 794 c = self.load_default_context()
796 795
797 796 c.user = self.db_user
798 797
799 798 c.active = 'ssh_keys_generate'
800 799 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
801 800 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
802 801
803 802 return self._get_template_context(c)
804 803
805 804 @LoginRequired()
806 805 @HasPermissionAllDecorator('hg.admin')
807 806 @CSRFRequired()
808 807 @view_config(
809 808 route_name='edit_user_ssh_keys_add', request_method='POST')
810 809 def ssh_keys_add(self):
811 810 _ = self.request.translate
812 811 c = self.load_default_context()
813 812
814 813 user_id = self.db_user_id
815 814 c.user = self.db_user
816 815
817 816 user_data = c.user.get_api_data()
818 817 key_data = self.request.POST.get('key_data')
819 818 description = self.request.POST.get('description')
820 819
821 820 try:
822 821 if not key_data:
823 822 raise ValueError('Please add a valid public key')
824 823
825 824 key = SshKeyModel().parse_key(key_data.strip())
826 825 fingerprint = key.hash_md5()
827 826
828 827 ssh_key = SshKeyModel().create(
829 828 c.user.user_id, fingerprint, key_data, description)
830 829 ssh_key_data = ssh_key.get_api_data()
831 830
832 831 audit_logger.store_web(
833 832 'user.edit.ssh_key.add', action_data={
834 833 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
835 834 user=self._rhodecode_user, )
836 835 Session().commit()
837 836
838 837 # Trigger an event on change of keys.
839 838 trigger(SshKeyFileChangeEvent(), self.request.registry)
840 839
841 840 h.flash(_("Ssh Key successfully created"), category='success')
842 841
843 842 except IntegrityError:
844 843 log.exception("Exception during ssh key saving")
845 844 h.flash(_('An error occurred during ssh key saving: {}').format(
846 845 'Such key already exists, please use a different one'),
847 846 category='error')
848 847 except Exception as e:
849 848 log.exception("Exception during ssh key saving")
850 849 h.flash(_('An error occurred during ssh key saving: {}').format(e),
851 850 category='error')
852 851
853 852 return HTTPFound(
854 853 h.route_path('edit_user_ssh_keys', user_id=user_id))
855 854
856 855 @LoginRequired()
857 856 @HasPermissionAllDecorator('hg.admin')
858 857 @CSRFRequired()
859 858 @view_config(
860 859 route_name='edit_user_ssh_keys_delete', request_method='POST')
861 860 def ssh_keys_delete(self):
862 861 _ = self.request.translate
863 862 c = self.load_default_context()
864 863
865 864 user_id = self.db_user_id
866 865 c.user = self.db_user
867 866
868 867 user_data = c.user.get_api_data()
869 868
870 869 del_ssh_key = self.request.POST.get('del_ssh_key')
871 870
872 871 if del_ssh_key:
873 872 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
874 873 ssh_key_data = ssh_key.get_api_data()
875 874
876 875 SshKeyModel().delete(del_ssh_key, c.user.user_id)
877 876 audit_logger.store_web(
878 877 'user.edit.ssh_key.delete', action_data={
879 878 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
880 879 user=self._rhodecode_user,)
881 880 Session().commit()
882 881 # Trigger an event on change of keys.
883 882 trigger(SshKeyFileChangeEvent(), self.request.registry)
884 883 h.flash(_("Ssh key successfully deleted"), category='success')
885 884
886 885 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
887 886
888 887 @LoginRequired()
889 888 @HasPermissionAllDecorator('hg.admin')
890 889 @view_config(
891 890 route_name='edit_user_emails', request_method='GET',
892 891 renderer='rhodecode:templates/admin/users/user_edit.mako')
893 892 def emails(self):
894 893 _ = self.request.translate
895 894 c = self.load_default_context()
896 895 c.user = self.db_user
897 896
898 897 c.active = 'emails'
899 898 c.user_email_map = UserEmailMap.query() \
900 899 .filter(UserEmailMap.user == c.user).all()
901 900
902 901 return self._get_template_context(c)
903 902
904 903 @LoginRequired()
905 904 @HasPermissionAllDecorator('hg.admin')
906 905 @CSRFRequired()
907 906 @view_config(
908 907 route_name='edit_user_emails_add', request_method='POST')
909 908 def emails_add(self):
910 909 _ = self.request.translate
911 910 c = self.load_default_context()
912 911
913 912 user_id = self.db_user_id
914 913 c.user = self.db_user
915 914
916 915 email = self.request.POST.get('new_email')
917 916 user_data = c.user.get_api_data()
918 917 try:
919 918
920 919 form = UserExtraEmailForm(self.request.translate)()
921 920 data = form.to_python({'email': email})
922 921 email = data['email']
923 922
924 923 UserModel().add_extra_email(c.user.user_id, email)
925 924 audit_logger.store_web(
926 925 'user.edit.email.add',
927 926 action_data={'email': email, 'user': user_data},
928 927 user=self._rhodecode_user)
929 928 Session().commit()
930 929 h.flash(_("Added new email address `%s` for user account") % email,
931 930 category='success')
932 931 except formencode.Invalid as error:
933 932 h.flash(h.escape(error.error_dict['email']), category='error')
934 933 except IntegrityError:
935 934 log.warning("Email %s already exists", email)
936 935 h.flash(_('Email `{}` is already registered for another user.').format(email),
937 936 category='error')
938 937 except Exception:
939 938 log.exception("Exception during email saving")
940 939 h.flash(_('An error occurred during email saving'),
941 940 category='error')
942 941 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
943 942
944 943 @LoginRequired()
945 944 @HasPermissionAllDecorator('hg.admin')
946 945 @CSRFRequired()
947 946 @view_config(
948 947 route_name='edit_user_emails_delete', request_method='POST')
949 948 def emails_delete(self):
950 949 _ = self.request.translate
951 950 c = self.load_default_context()
952 951
953 952 user_id = self.db_user_id
954 953 c.user = self.db_user
955 954
956 955 email_id = self.request.POST.get('del_email_id')
957 956 user_model = UserModel()
958 957
959 958 email = UserEmailMap.query().get(email_id).email
960 959 user_data = c.user.get_api_data()
961 960 user_model.delete_extra_email(c.user.user_id, email_id)
962 961 audit_logger.store_web(
963 962 'user.edit.email.delete',
964 963 action_data={'email': email, 'user': user_data},
965 964 user=self._rhodecode_user)
966 965 Session().commit()
967 966 h.flash(_("Removed email address from user account"),
968 967 category='success')
969 968 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
970 969
971 970 @LoginRequired()
972 971 @HasPermissionAllDecorator('hg.admin')
973 972 @view_config(
974 973 route_name='edit_user_ips', request_method='GET',
975 974 renderer='rhodecode:templates/admin/users/user_edit.mako')
976 975 def ips(self):
977 976 _ = self.request.translate
978 977 c = self.load_default_context()
979 978 c.user = self.db_user
980 979
981 980 c.active = 'ips'
982 981 c.user_ip_map = UserIpMap.query() \
983 982 .filter(UserIpMap.user == c.user).all()
984 983
985 984 c.inherit_default_ips = c.user.inherit_default_permissions
986 985 c.default_user_ip_map = UserIpMap.query() \
987 986 .filter(UserIpMap.user == User.get_default_user()).all()
988 987
989 988 return self._get_template_context(c)
990 989
991 990 @LoginRequired()
992 991 @HasPermissionAllDecorator('hg.admin')
993 992 @CSRFRequired()
994 993 @view_config(
995 994 route_name='edit_user_ips_add', request_method='POST')
996 995 # NOTE(marcink): this view is allowed for default users, as we can
997 996 # edit their IP white list
998 997 def ips_add(self):
999 998 _ = self.request.translate
1000 999 c = self.load_default_context()
1001 1000
1002 1001 user_id = self.db_user_id
1003 1002 c.user = self.db_user
1004 1003
1005 1004 user_model = UserModel()
1006 1005 desc = self.request.POST.get('description')
1007 1006 try:
1008 1007 ip_list = user_model.parse_ip_range(
1009 1008 self.request.POST.get('new_ip'))
1010 1009 except Exception as e:
1011 1010 ip_list = []
1012 1011 log.exception("Exception during ip saving")
1013 1012 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1014 1013 category='error')
1015 1014 added = []
1016 1015 user_data = c.user.get_api_data()
1017 1016 for ip in ip_list:
1018 1017 try:
1019 1018 form = UserExtraIpForm(self.request.translate)()
1020 1019 data = form.to_python({'ip': ip})
1021 1020 ip = data['ip']
1022 1021
1023 1022 user_model.add_extra_ip(c.user.user_id, ip, desc)
1024 1023 audit_logger.store_web(
1025 1024 'user.edit.ip.add',
1026 1025 action_data={'ip': ip, 'user': user_data},
1027 1026 user=self._rhodecode_user)
1028 1027 Session().commit()
1029 1028 added.append(ip)
1030 1029 except formencode.Invalid as error:
1031 1030 msg = error.error_dict['ip']
1032 1031 h.flash(msg, category='error')
1033 1032 except Exception:
1034 1033 log.exception("Exception during ip saving")
1035 1034 h.flash(_('An error occurred during ip saving'),
1036 1035 category='error')
1037 1036 if added:
1038 1037 h.flash(
1039 1038 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1040 1039 category='success')
1041 1040 if 'default_user' in self.request.POST:
1042 1041 # case for editing global IP list we do it for 'DEFAULT' user
1043 1042 raise HTTPFound(h.route_path('admin_permissions_ips'))
1044 1043 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1045 1044
1046 1045 @LoginRequired()
1047 1046 @HasPermissionAllDecorator('hg.admin')
1048 1047 @CSRFRequired()
1049 1048 @view_config(
1050 1049 route_name='edit_user_ips_delete', request_method='POST')
1051 1050 # NOTE(marcink): this view is allowed for default users, as we can
1052 1051 # edit their IP white list
1053 1052 def ips_delete(self):
1054 1053 _ = self.request.translate
1055 1054 c = self.load_default_context()
1056 1055
1057 1056 user_id = self.db_user_id
1058 1057 c.user = self.db_user
1059 1058
1060 1059 ip_id = self.request.POST.get('del_ip_id')
1061 1060 user_model = UserModel()
1062 1061 user_data = c.user.get_api_data()
1063 1062 ip = UserIpMap.query().get(ip_id).ip_addr
1064 1063 user_model.delete_extra_ip(c.user.user_id, ip_id)
1065 1064 audit_logger.store_web(
1066 1065 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1067 1066 user=self._rhodecode_user)
1068 1067 Session().commit()
1069 1068 h.flash(_("Removed ip address from user whitelist"), category='success')
1070 1069
1071 1070 if 'default_user' in self.request.POST:
1072 1071 # case for editing global IP list we do it for 'DEFAULT' user
1073 1072 raise HTTPFound(h.route_path('admin_permissions_ips'))
1074 1073 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1075 1074
1076 1075 @LoginRequired()
1077 1076 @HasPermissionAllDecorator('hg.admin')
1078 1077 @view_config(
1079 1078 route_name='edit_user_groups_management', request_method='GET',
1080 1079 renderer='rhodecode:templates/admin/users/user_edit.mako')
1081 1080 def groups_management(self):
1082 1081 c = self.load_default_context()
1083 1082 c.user = self.db_user
1084 1083 c.data = c.user.group_member
1085 1084
1086 1085 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1087 1086 for group in c.user.group_member]
1088 1087 c.groups = json.dumps(groups)
1089 1088 c.active = 'groups'
1090 1089
1091 1090 return self._get_template_context(c)
1092 1091
1093 1092 @LoginRequired()
1094 1093 @HasPermissionAllDecorator('hg.admin')
1095 1094 @CSRFRequired()
1096 1095 @view_config(
1097 1096 route_name='edit_user_groups_management_updates', request_method='POST')
1098 1097 def groups_management_updates(self):
1099 1098 _ = self.request.translate
1100 1099 c = self.load_default_context()
1101 1100
1102 1101 user_id = self.db_user_id
1103 1102 c.user = self.db_user
1104 1103
1105 1104 user_groups = set(self.request.POST.getall('users_group_id'))
1106 1105 user_groups_objects = []
1107 1106
1108 1107 for ugid in user_groups:
1109 1108 user_groups_objects.append(
1110 1109 UserGroupModel().get_group(safe_int(ugid)))
1111 1110 user_group_model = UserGroupModel()
1112 1111 added_to_groups, removed_from_groups = \
1113 1112 user_group_model.change_groups(c.user, user_groups_objects)
1114 1113
1115 1114 user_data = c.user.get_api_data()
1116 1115 for user_group_id in added_to_groups:
1117 1116 user_group = UserGroup.get(user_group_id)
1118 1117 old_values = user_group.get_api_data()
1119 1118 audit_logger.store_web(
1120 1119 'user_group.edit.member.add',
1121 1120 action_data={'user': user_data, 'old_data': old_values},
1122 1121 user=self._rhodecode_user)
1123 1122
1124 1123 for user_group_id in removed_from_groups:
1125 1124 user_group = UserGroup.get(user_group_id)
1126 1125 old_values = user_group.get_api_data()
1127 1126 audit_logger.store_web(
1128 1127 'user_group.edit.member.delete',
1129 1128 action_data={'user': user_data, 'old_data': old_values},
1130 1129 user=self._rhodecode_user)
1131 1130
1132 1131 Session().commit()
1133 1132 c.active = 'user_groups_management'
1134 1133 h.flash(_("Groups successfully changed"), category='success')
1135 1134
1136 1135 return HTTPFound(h.route_path(
1137 1136 'edit_user_groups_management', user_id=user_id))
1138 1137
1139 1138 @LoginRequired()
1140 1139 @HasPermissionAllDecorator('hg.admin')
1141 1140 @view_config(
1142 1141 route_name='edit_user_audit_logs', request_method='GET',
1143 1142 renderer='rhodecode:templates/admin/users/user_edit.mako')
1144 1143 def user_audit_logs(self):
1145 1144 _ = self.request.translate
1146 1145 c = self.load_default_context()
1147 1146 c.user = self.db_user
1148 1147
1149 1148 c.active = 'audit'
1150 1149
1151 1150 p = safe_int(self.request.GET.get('page', 1), 1)
1152 1151
1153 1152 filter_term = self.request.GET.get('filter')
1154 1153 user_log = UserModel().get_user_log(c.user, filter_term)
1155 1154
1156 1155 def url_generator(**kw):
1157 1156 if filter_term:
1158 1157 kw['filter'] = filter_term
1159 1158 return self.request.current_route_path(_query=kw)
1160 1159
1161 1160 c.audit_logs = h.Page(
1162 1161 user_log, page=p, items_per_page=10, url=url_generator)
1163 1162 c.filter_term = filter_term
1164 1163 return self._get_template_context(c)
1165 1164
1166 1165 @LoginRequired()
1167 1166 @HasPermissionAllDecorator('hg.admin')
1168 1167 @view_config(
1169 1168 route_name='edit_user_perms_summary', request_method='GET',
1170 1169 renderer='rhodecode:templates/admin/users/user_edit.mako')
1171 1170 def user_perms_summary(self):
1172 1171 _ = self.request.translate
1173 1172 c = self.load_default_context()
1174 1173 c.user = self.db_user
1175 1174
1176 1175 c.active = 'perms_summary'
1177 1176 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1178 1177
1179 1178 return self._get_template_context(c)
1180 1179
1181 1180 @LoginRequired()
1182 1181 @HasPermissionAllDecorator('hg.admin')
1183 1182 @view_config(
1184 1183 route_name='edit_user_perms_summary_json', request_method='GET',
1185 1184 renderer='json_ext')
1186 1185 def user_perms_summary_json(self):
1187 1186 self.load_default_context()
1188 1187 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1189 1188
1190 1189 return perm_user.permissions
@@ -1,553 +1,559 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.tests import (
27 27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 28 no_newline_id_generator)
29 29 from rhodecode.tests.fixture import Fixture
30 30 from rhodecode.lib.auth import check_password
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.model.auth_token import AuthTokenModel
33 33 from rhodecode.model.db import User, Notification, UserApiKeys
34 34 from rhodecode.model.meta import Session
35 35
36 36 fixture = Fixture()
37 37
38 38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 39
40 40
41 41 def route_path(name, params=None, **kwargs):
42 42 import urllib
43 43 from rhodecode.apps._base import ADMIN_PREFIX
44 44
45 45 base_url = {
46 46 'login': ADMIN_PREFIX + '/login',
47 47 'logout': ADMIN_PREFIX + '/logout',
48 48 'register': ADMIN_PREFIX + '/register',
49 49 'reset_password':
50 50 ADMIN_PREFIX + '/password_reset',
51 51 'reset_password_confirmation':
52 52 ADMIN_PREFIX + '/password_reset_confirmation',
53 53
54 54 'admin_permissions_application':
55 55 ADMIN_PREFIX + '/permissions/application',
56 56 'admin_permissions_application_update':
57 57 ADMIN_PREFIX + '/permissions/application/update',
58 58
59 59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60 60
61 61 }[name].format(**kwargs)
62 62
63 63 if params:
64 64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
65 65 return base_url
66 66
67 67
68 68 @pytest.mark.usefixtures('app')
69 69 class TestLoginController(object):
70 70 destroy_users = set()
71 71
72 72 @classmethod
73 73 def teardown_class(cls):
74 74 fixture.destroy_users(cls.destroy_users)
75 75
76 76 def teardown_method(self, method):
77 77 for n in Notification.query().all():
78 78 Session().delete(n)
79 79
80 80 Session().commit()
81 81 assert Notification.query().all() == []
82 82
83 83 def test_index(self):
84 84 response = self.app.get(route_path('login'))
85 85 assert response.status == '200 OK'
86 86 # Test response...
87 87
88 88 def test_login_admin_ok(self):
89 89 response = self.app.post(route_path('login'),
90 90 {'username': 'test_admin',
91 'password': 'test12'})
92 assert response.status == '302 Found'
91 'password': 'test12'}, status=302)
92 response = response.follow()
93 93 session = response.get_session_from_response()
94 94 username = session['rhodecode_user'].get('username')
95 95 assert username == 'test_admin'
96 response = response.follow()
97 96 response.mustcontain('/%s' % HG_REPO)
98 97
99 98 def test_login_regular_ok(self):
100 99 response = self.app.post(route_path('login'),
101 100 {'username': 'test_regular',
102 'password': 'test12'})
101 'password': 'test12'}, status=302)
103 102
104 assert response.status == '302 Found'
103 response = response.follow()
105 104 session = response.get_session_from_response()
106 105 username = session['rhodecode_user'].get('username')
107 106 assert username == 'test_regular'
108 response = response.follow()
107
109 108 response.mustcontain('/%s' % HG_REPO)
110 109
111 110 def test_login_ok_came_from(self):
112 111 test_came_from = '/_admin/users?branch=stable'
113 112 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
114 113 response = self.app.post(
115 _url, {'username': 'test_admin', 'password': 'test12'})
116 assert response.status == '302 Found'
114 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
115
117 116 assert 'branch=stable' in response.location
118 117 response = response.follow()
119 118
120 119 assert response.status == '200 OK'
121 120 response.mustcontain('Users administration')
122 121
123 122 def test_redirect_to_login_with_get_args(self):
124 123 with fixture.anon_access(False):
125 124 kwargs = {'branch': 'stable'}
126 125 response = self.app.get(
127 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs))
128 assert response.status == '302 Found'
126 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
127 status=302)
129 128
130 129 response_query = urlparse.parse_qsl(response.location)
131 130 assert 'branch=stable' in response_query[0][1]
132 131
133 132 def test_login_form_with_get_args(self):
134 133 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
135 134 response = self.app.get(_url)
136 135 assert 'branch%3Dstable' in response.form.action
137 136
138 137 @pytest.mark.parametrize("url_came_from", [
139 138 'data:text/html,<script>window.alert("xss")</script>',
140 139 'mailto:test@rhodecode.org',
141 140 'file:///etc/passwd',
142 141 'ftp://some.ftp.server',
143 142 'http://other.domain',
144 143 '/\r\nX-Forwarded-Host: http://example.org',
145 144 ], ids=no_newline_id_generator)
146 145 def test_login_bad_came_froms(self, url_came_from):
147 146 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
148 147 response = self.app.post(
149 148 _url,
150 149 {'username': 'test_admin', 'password': 'test12'})
151 150 assert response.status == '302 Found'
152 151 response = response.follow()
153 152 assert response.status == '200 OK'
154 153 assert response.request.path == '/'
155 154
156 155 def test_login_short_password(self):
157 156 response = self.app.post(route_path('login'),
158 157 {'username': 'test_admin',
159 158 'password': 'as'})
160 159 assert response.status == '200 OK'
161 160
162 161 response.mustcontain('Enter 3 characters or more')
163 162
164 163 def test_login_wrong_non_ascii_password(self, user_regular):
165 164 response = self.app.post(
166 165 route_path('login'),
167 166 {'username': user_regular.username,
168 167 'password': u'invalid-non-asci\xe4'.encode('utf8')})
169 168
170 169 response.mustcontain('invalid user name')
171 170 response.mustcontain('invalid password')
172 171
173 172 def test_login_with_non_ascii_password(self, user_util):
174 173 password = u'valid-non-ascii\xe4'
175 174 user = user_util.create_user(password=password)
176 175 response = self.app.post(
177 176 route_path('login'),
178 177 {'username': user.username,
179 178 'password': password.encode('utf-8')})
180 179 assert response.status_code == 302
181 180
182 181 def test_login_wrong_username_password(self):
183 182 response = self.app.post(route_path('login'),
184 183 {'username': 'error',
185 184 'password': 'test12'})
186 185
187 186 response.mustcontain('invalid user name')
188 187 response.mustcontain('invalid password')
189 188
190 189 def test_login_admin_ok_password_migration(self, real_crypto_backend):
191 190 from rhodecode.lib import auth
192 191
193 192 # create new user, with sha256 password
194 193 temp_user = 'test_admin_sha256'
195 194 user = fixture.create_user(temp_user)
196 195 user.password = auth._RhodeCodeCryptoSha256().hash_create(
197 196 b'test123')
198 197 Session().add(user)
199 198 Session().commit()
200 199 self.destroy_users.add(temp_user)
201 200 response = self.app.post(route_path('login'),
202 201 {'username': temp_user,
203 'password': 'test123'})
202 'password': 'test123'}, status=302)
204 203
205 assert response.status == '302 Found'
204 response = response.follow()
206 205 session = response.get_session_from_response()
207 206 username = session['rhodecode_user'].get('username')
208 207 assert username == temp_user
209 response = response.follow()
210 208 response.mustcontain('/%s' % HG_REPO)
211 209
212 210 # new password should be bcrypted, after log-in and transfer
213 211 user = User.get_by_username(temp_user)
214 212 assert user.password.startswith('$')
215 213
216 214 # REGISTRATIONS
217 215 def test_register(self):
218 216 response = self.app.get(route_path('register'))
219 217 response.mustcontain('Create an Account')
220 218
221 219 def test_register_err_same_username(self):
222 220 uname = 'test_admin'
223 221 response = self.app.post(
224 222 route_path('register'),
225 223 {
226 224 'username': uname,
227 225 'password': 'test12',
228 226 'password_confirmation': 'test12',
229 227 'email': 'goodmail@domain.com',
230 228 'firstname': 'test',
231 229 'lastname': 'test'
232 230 }
233 231 )
234 232
235 233 assertr = response.assert_response()
236 msg = '???'
234 msg = 'Username "%(username)s" already exists'
237 235 msg = msg % {'username': uname}
238 236 assertr.element_contains('#username+.error-message', msg)
239 237
240 238 def test_register_err_same_email(self):
241 239 response = self.app.post(
242 240 route_path('register'),
243 241 {
244 242 'username': 'test_admin_0',
245 243 'password': 'test12',
246 244 'password_confirmation': 'test12',
247 245 'email': 'test_admin@mail.com',
248 246 'firstname': 'test',
249 247 'lastname': 'test'
250 248 }
251 249 )
252 250
253 251 assertr = response.assert_response()
254 msg = '???'
252 msg = u'This e-mail address is already taken'
255 253 assertr.element_contains('#email+.error-message', msg)
256 254
257 255 def test_register_err_same_email_case_sensitive(self):
258 256 response = self.app.post(
259 257 route_path('register'),
260 258 {
261 259 'username': 'test_admin_1',
262 260 'password': 'test12',
263 261 'password_confirmation': 'test12',
264 262 'email': 'TesT_Admin@mail.COM',
265 263 'firstname': 'test',
266 264 'lastname': 'test'
267 265 }
268 266 )
269 267 assertr = response.assert_response()
270 msg = '???'
268 msg = u'This e-mail address is already taken'
271 269 assertr.element_contains('#email+.error-message', msg)
272 270
273 271 def test_register_err_wrong_data(self):
274 272 response = self.app.post(
275 273 route_path('register'),
276 274 {
277 275 'username': 'xs',
278 276 'password': 'test',
279 277 'password_confirmation': 'test',
280 278 'email': 'goodmailm',
281 279 'firstname': 'test',
282 280 'lastname': 'test'
283 281 }
284 282 )
285 283 assert response.status == '200 OK'
286 284 response.mustcontain('An email address must contain a single @')
287 285 response.mustcontain('Enter a value 6 characters long or more')
288 286
289 287 def test_register_err_username(self):
290 288 response = self.app.post(
291 289 route_path('register'),
292 290 {
293 291 'username': 'error user',
294 292 'password': 'test12',
295 293 'password_confirmation': 'test12',
296 294 'email': 'goodmailm',
297 295 'firstname': 'test',
298 296 'lastname': 'test'
299 297 }
300 298 )
301 299
302 300 response.mustcontain('An email address must contain a single @')
303 301 response.mustcontain(
304 302 'Username may only contain '
305 303 'alphanumeric characters underscores, '
306 304 'periods or dashes and must begin with '
307 305 'alphanumeric character')
308 306
309 307 def test_register_err_case_sensitive(self):
310 308 usr = 'Test_Admin'
311 309 response = self.app.post(
312 310 route_path('register'),
313 311 {
314 312 'username': usr,
315 313 'password': 'test12',
316 314 'password_confirmation': 'test12',
317 315 'email': 'goodmailm',
318 316 'firstname': 'test',
319 317 'lastname': 'test'
320 318 }
321 319 )
322 320
323 321 assertr = response.assert_response()
324 msg = '???'
322 msg = u'Username "%(username)s" already exists'
325 323 msg = msg % {'username': usr}
326 324 assertr.element_contains('#username+.error-message', msg)
327 325
328 326 def test_register_special_chars(self):
329 327 response = self.app.post(
330 328 route_path('register'),
331 329 {
332 330 'username': 'xxxaxn',
333 331 'password': 'ąćźżąśśśś',
334 332 'password_confirmation': 'ąćźżąśśśś',
335 333 'email': 'goodmailm@test.plx',
336 334 'firstname': 'test',
337 335 'lastname': 'test'
338 336 }
339 337 )
340 338
341 msg = '???'
339 msg = u'Invalid characters (non-ascii) in password'
342 340 response.mustcontain(msg)
343 341
344 342 def test_register_password_mismatch(self):
345 343 response = self.app.post(
346 344 route_path('register'),
347 345 {
348 346 'username': 'xs',
349 347 'password': '123qwe',
350 348 'password_confirmation': 'qwe123',
351 349 'email': 'goodmailm@test.plxa',
352 350 'firstname': 'test',
353 351 'lastname': 'test'
354 352 }
355 353 )
356 msg = '???'
354 msg = u'Passwords do not match'
357 355 response.mustcontain(msg)
358 356
359 357 def test_register_ok(self):
360 358 username = 'test_regular4'
361 359 password = 'qweqwe'
362 360 email = 'marcin@test.com'
363 361 name = 'testname'
364 362 lastname = 'testlastname'
365 363
364 # this initializes a session
365 response = self.app.get(route_path('register'))
366 response.mustcontain('Create an Account')
367
368
366 369 response = self.app.post(
367 370 route_path('register'),
368 371 {
369 372 'username': username,
370 373 'password': password,
371 374 'password_confirmation': password,
372 375 'email': email,
373 376 'firstname': name,
374 377 'lastname': lastname,
375 378 'admin': True
376 }
377 ) # This should be overriden
378 assert response.status == '302 Found'
379 },
380 status=302
381 ) # This should be overridden
382
379 383 assert_session_flash(
380 384 response, 'You have successfully registered with RhodeCode')
381 385
382 386 ret = Session().query(User).filter(
383 387 User.username == 'test_regular4').one()
384 388 assert ret.username == username
385 389 assert check_password(password, ret.password)
386 390 assert ret.email == email
387 391 assert ret.name == name
388 392 assert ret.lastname == lastname
389 393 assert ret.auth_tokens is not None
390 394 assert not ret.admin
391 395
392 396 def test_forgot_password_wrong_mail(self):
393 397 bad_email = 'marcin@wrongmail.org'
398 # this initializes a session
399 self.app.get(route_path('reset_password'))
400
394 401 response = self.app.post(
395 402 route_path('reset_password'), {'email': bad_email, }
396 403 )
397 404 assert_session_flash(response,
398 405 'If such email exists, a password reset link was sent to it.')
399 406
400 407 def test_forgot_password(self, user_util):
401 response = self.app.get(route_path('reset_password'))
402 assert response.status == '200 OK'
408 # this initializes a session
409 self.app.get(route_path('reset_password'))
403 410
404 411 user = user_util.create_user()
405 412 user_id = user.user_id
406 413 email = user.email
407 414
408 415 response = self.app.post(route_path('reset_password'), {'email': email, })
409 416
410 417 assert_session_flash(response,
411 418 'If such email exists, a password reset link was sent to it.')
412 419
413 420 # BAD KEY
414 421 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
415 response = self.app.get(confirm_url)
416 assert response.status == '302 Found'
422 response = self.app.get(confirm_url, status=302)
417 423 assert response.location.endswith(route_path('reset_password'))
418 424 assert_session_flash(response, 'Given reset token is invalid')
419 425
420 426 response.follow() # cleanup flash
421 427
422 428 # GOOD KEY
423 429 key = UserApiKeys.query()\
424 430 .filter(UserApiKeys.user_id == user_id)\
425 431 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
426 432 .first()
427 433
428 434 assert key
429 435
430 436 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
431 437 response = self.app.get(confirm_url)
432 438 assert response.status == '302 Found'
433 439 assert response.location.endswith(route_path('login'))
434 440
435 441 assert_session_flash(
436 442 response,
437 443 'Your password reset was successful, '
438 444 'a new password has been sent to your email')
439 445
440 446 response.follow()
441 447
442 448 def _get_api_whitelist(self, values=None):
443 449 config = {'api_access_controllers_whitelist': values or []}
444 450 return config
445 451
446 452 @pytest.mark.parametrize("test_name, auth_token", [
447 453 ('none', None),
448 454 ('empty_string', ''),
449 455 ('fake_number', '123456'),
450 456 ('proper_auth_token', None)
451 457 ])
452 458 def test_access_not_whitelisted_page_via_auth_token(
453 459 self, test_name, auth_token, user_admin):
454 460
455 461 whitelist = self._get_api_whitelist([])
456 462 with mock.patch.dict('rhodecode.CONFIG', whitelist):
457 463 assert [] == whitelist['api_access_controllers_whitelist']
458 464 if test_name == 'proper_auth_token':
459 465 # use builtin if api_key is None
460 466 auth_token = user_admin.api_key
461 467
462 468 with fixture.anon_access(False):
463 469 self.app.get(
464 470 route_path('repo_commit_raw',
465 471 repo_name=HG_REPO, commit_id='tip',
466 472 params=dict(api_key=auth_token)),
467 473 status=302)
468 474
469 475 @pytest.mark.parametrize("test_name, auth_token, code", [
470 476 ('none', None, 302),
471 477 ('empty_string', '', 302),
472 478 ('fake_number', '123456', 302),
473 479 ('proper_auth_token', None, 200)
474 480 ])
475 481 def test_access_whitelisted_page_via_auth_token(
476 482 self, test_name, auth_token, code, user_admin):
477 483
478 484 whitelist = self._get_api_whitelist(whitelist_view)
479 485
480 486 with mock.patch.dict('rhodecode.CONFIG', whitelist):
481 487 assert whitelist_view == whitelist['api_access_controllers_whitelist']
482 488
483 489 if test_name == 'proper_auth_token':
484 490 auth_token = user_admin.api_key
485 491 assert auth_token
486 492
487 493 with fixture.anon_access(False):
488 494 self.app.get(
489 495 route_path('repo_commit_raw',
490 496 repo_name=HG_REPO, commit_id='tip',
491 497 params=dict(api_key=auth_token)),
492 498 status=code)
493 499
494 500 @pytest.mark.parametrize("test_name, auth_token, code", [
495 501 ('proper_auth_token', None, 200),
496 502 ('wrong_auth_token', '123456', 302),
497 503 ])
498 504 def test_access_whitelisted_page_via_auth_token_bound_to_token(
499 505 self, test_name, auth_token, code, user_admin):
500 506
501 507 expected_token = auth_token
502 508 if test_name == 'proper_auth_token':
503 509 auth_token = user_admin.api_key
504 510 expected_token = auth_token
505 511 assert auth_token
506 512
507 513 whitelist = self._get_api_whitelist([
508 514 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
509 515
510 516 with mock.patch.dict('rhodecode.CONFIG', whitelist):
511 517
512 518 with fixture.anon_access(False):
513 519 self.app.get(
514 520 route_path('repo_commit_raw',
515 521 repo_name=HG_REPO, commit_id='tip',
516 522 params=dict(api_key=auth_token)),
517 523 status=code)
518 524
519 525 def test_access_page_via_extra_auth_token(self):
520 526 whitelist = self._get_api_whitelist(whitelist_view)
521 527 with mock.patch.dict('rhodecode.CONFIG', whitelist):
522 528 assert whitelist_view == \
523 529 whitelist['api_access_controllers_whitelist']
524 530
525 531 new_auth_token = AuthTokenModel().create(
526 532 TEST_USER_ADMIN_LOGIN, 'test')
527 533 Session().commit()
528 534 with fixture.anon_access(False):
529 535 self.app.get(
530 536 route_path('repo_commit_raw',
531 537 repo_name=HG_REPO, commit_id='tip',
532 538 params=dict(api_key=new_auth_token.api_key)),
533 539 status=200)
534 540
535 541 def test_access_page_via_expired_auth_token(self):
536 542 whitelist = self._get_api_whitelist(whitelist_view)
537 543 with mock.patch.dict('rhodecode.CONFIG', whitelist):
538 544 assert whitelist_view == \
539 545 whitelist['api_access_controllers_whitelist']
540 546
541 547 new_auth_token = AuthTokenModel().create(
542 548 TEST_USER_ADMIN_LOGIN, 'test')
543 549 Session().commit()
544 550 # patch the api key and make it expired
545 551 new_auth_token.expires = 0
546 552 Session().add(new_auth_token)
547 553 Session().commit()
548 554 with fixture.anon_access(False):
549 555 self.app.get(
550 556 route_path('repo_commit_raw',
551 557 repo_name=HG_REPO, commit_id='tip',
552 558 params=dict(api_key=new_auth_token.api_key)),
553 559 status=302)
@@ -1,133 +1,133 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
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 26 from rhodecode.apps.login.views import LoginView, CaptchaData
27 27 from rhodecode.model.settings import SettingsModel
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.tests.utils import AssertResponse
30 30
31 31
32 32 class RhodeCodeSetting(object):
33 33 def __init__(self, name, value):
34 34 self.name = name
35 35 self.value = value
36 36
37 37 def __enter__(self):
38 38 from rhodecode.model.settings import SettingsModel
39 39 model = SettingsModel()
40 40 self.old_setting = model.get_setting_by_name(self.name)
41 41 model.create_or_update_setting(name=self.name, val=self.value)
42 42 return self
43 43
44 44 def __exit__(self, exc_type, exc_val, exc_tb):
45 45 model = SettingsModel()
46 46 if self.old_setting:
47 47 model.create_or_update_setting(
48 48 name=self.name, val=self.old_setting.app_settings_value)
49 49 else:
50 50 model.create_or_update_setting(name=self.name)
51 51
52 52
53 53 class TestRegisterCaptcha(object):
54 54
55 55 @pytest.mark.parametrize('private_key, public_key, expected', [
56 56 ('', '', CaptchaData(False, '', '')),
57 57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
58 58 ('privkey', '', CaptchaData(True, 'privkey', '')),
59 59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
60 60 ])
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
61 def test_get_captcha_data(self, private_key, public_key, expected,
62 62 request_stub, user_util):
63 63 request_stub.user = user_util.create_user().AuthUser()
64 64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 65 login_view = LoginView(mock.Mock(), request_stub)
66 66
67 67 with RhodeCodeSetting('captcha_private_key', private_key):
68 68 with RhodeCodeSetting('captcha_public_key', public_key):
69 69 captcha = login_view._get_captcha_data()
70 70 assert captcha == expected
71 71
72 72 @pytest.mark.parametrize('active', [False, True])
73 73 @mock.patch.object(LoginView, '_get_captcha_data')
74 74 def test_private_key_does_not_leak_to_html(
75 75 self, m_get_captcha_data, active, app):
76 76 captcha = CaptchaData(
77 77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
78 78 m_get_captcha_data.return_value = captcha
79 79
80 80 response = app.get(ADMIN_PREFIX + '/register')
81 81 assert 'PRIVATE_KEY' not in response
82 82
83 83 @pytest.mark.parametrize('active', [False, True])
84 84 @mock.patch.object(LoginView, '_get_captcha_data')
85 85 def test_register_view_renders_captcha(
86 86 self, m_get_captcha_data, active, app):
87 87 captcha = CaptchaData(
88 88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
89 89 m_get_captcha_data.return_value = captcha
90 90
91 91 response = app.get(ADMIN_PREFIX + '/register')
92 92
93 93 assertr = AssertResponse(response)
94 94 if active:
95 95 assertr.one_element_exists('#recaptcha_field')
96 96 else:
97 97 assertr.no_element_exists('#recaptcha_field')
98 98
99 99 @pytest.mark.parametrize('valid', [False, True])
100 100 @mock.patch('rhodecode.apps.login.views.submit')
101 101 @mock.patch.object(LoginView, '_get_captcha_data')
102 102 def test_register_with_active_captcha(
103 103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
104 104 captcha = CaptchaData(
105 105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
106 106 m_get_captcha_data.return_value = captcha
107 107 m_response = mock.Mock()
108 108 m_response.is_valid = valid
109 109 m_submit.return_value = m_response
110 110
111 111 params = {
112 112 'csrf_token': csrf_token,
113 113 'email': 'pytest@example.com',
114 114 'firstname': 'pytest-firstname',
115 115 'lastname': 'pytest-lastname',
116 116 'password': 'secret',
117 117 'password_confirmation': 'secret',
118 118 'username': 'pytest',
119 119 }
120 120 response = app.post(ADMIN_PREFIX + '/register', params=params)
121 121
122 122 if valid:
123 123 # If we provided a valid captcha input we expect a successful
124 124 # registration and redirect to the login page.
125 125 assert response.status_int == 302
126 126 assert 'location' in response.headers
127 127 assert ADMIN_PREFIX + '/login' in response.headers['location']
128 128 else:
129 129 # If captche input is invalid we expect to stay on the registration
130 130 # page with an error message displayed.
131 131 assertr = AssertResponse(response)
132 132 assert response.status_int == 200
133 133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,426 +1,428 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 formencode.htmlfill
26 26 import logging
27 27 import urlparse
28 28
29 29 from pyramid.httpexceptions import HTTPFound
30 30 from pyramid.view import view_config
31 31 from recaptcha.client.captcha import submit
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 from rhodecode.events import UserRegistered
35 from rhodecode.events import UserRegistered, trigger
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.auth import (
39 39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
40 40 from rhodecode.lib.base import get_ip_addr
41 41 from rhodecode.lib.exceptions import UserCreationError
42 42 from rhodecode.lib.utils2 import safe_str
43 43 from rhodecode.model.db import User, UserApiKeys
44 44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.auth_token import AuthTokenModel
47 47 from rhodecode.model.settings import SettingsModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.translation import _
50 50
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54 CaptchaData = collections.namedtuple(
55 55 'CaptchaData', 'active, private_key, public_key')
56 56
57 57
58 58 def _store_user_in_session(session, username, remember=False):
59 59 user = User.get_by_username(username, case_insensitive=True)
60 60 auth_user = AuthUser(user.user_id)
61 61 auth_user.set_authenticated()
62 62 cs = auth_user.get_cookie_store()
63 63 session['rhodecode_user'] = cs
64 64 user.update_lastlogin()
65 65 Session().commit()
66 66
67 67 # If they want to be remembered, update the cookie
68 68 if remember:
69 69 _year = (datetime.datetime.now() +
70 70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
71 71 session._set_cookie_expires(_year)
72 72
73 73 session.save()
74 74
75 75 safe_cs = cs.copy()
76 76 safe_cs['password'] = '****'
77 77 log.info('user %s is now authenticated and stored in '
78 78 'session, session attrs %s', username, safe_cs)
79 79
80 80 # dumps session attrs back to cookie
81 81 session._update_cookie_out()
82 82 # we set new cookie
83 83 headers = None
84 84 if session.request['set_cookie']:
85 85 # send set-cookie headers back to response to update cookie
86 86 headers = [('Set-Cookie', session.request['cookie_out'])]
87 87 return headers
88 88
89 89
90 90 def get_came_from(request):
91 91 came_from = safe_str(request.GET.get('came_from', ''))
92 92 parsed = urlparse.urlparse(came_from)
93 93 allowed_schemes = ['http', 'https']
94 94 default_came_from = h.route_path('home')
95 95 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 96 log.error('Suspicious URL scheme detected %s for url %s' %
97 97 (parsed.scheme, parsed))
98 98 came_from = default_came_from
99 99 elif parsed.netloc and request.host != parsed.netloc:
100 100 log.error('Suspicious NETLOC detected %s for url %s server url '
101 101 'is: %s' % (parsed.netloc, parsed, request.host))
102 102 came_from = default_came_from
103 103 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
104 104 log.error('Header injection detected `%s` for url %s server url ' %
105 105 (parsed.path, parsed))
106 106 came_from = default_came_from
107 107
108 108 return came_from or default_came_from
109 109
110 110
111 111 class LoginView(BaseAppView):
112 112
113 113 def load_default_context(self):
114 114 c = self._get_local_tmpl_context()
115 115 c.came_from = get_came_from(self.request)
116 116
117 117 return c
118 118
119 119 def _get_captcha_data(self):
120 120 settings = SettingsModel().get_all_settings()
121 121 private_key = settings.get('rhodecode_captcha_private_key')
122 122 public_key = settings.get('rhodecode_captcha_public_key')
123 123 active = bool(private_key)
124 124 return CaptchaData(
125 125 active=active, private_key=private_key, public_key=public_key)
126 126
127 127 @view_config(
128 128 route_name='login', request_method='GET',
129 129 renderer='rhodecode:templates/login.mako')
130 130 def login(self):
131 131 c = self.load_default_context()
132 132 auth_user = self._rhodecode_user
133 133
134 134 # redirect if already logged in
135 135 if (auth_user.is_authenticated and
136 136 not auth_user.is_default and auth_user.ip_allowed):
137 137 raise HTTPFound(c.came_from)
138 138
139 139 # check if we use headers plugin, and try to login using it.
140 140 try:
141 141 log.debug('Running PRE-AUTH for headers based authentication')
142 142 auth_info = authenticate(
143 143 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
144 144 if auth_info:
145 145 headers = _store_user_in_session(
146 146 self.session, auth_info.get('username'))
147 147 raise HTTPFound(c.came_from, headers=headers)
148 148 except UserCreationError as e:
149 149 log.error(e)
150 self.session.flash(e, queue='error')
150 h.flash(e, category='error')
151 151
152 152 return self._get_template_context(c)
153 153
154 154 @view_config(
155 155 route_name='login', request_method='POST',
156 156 renderer='rhodecode:templates/login.mako')
157 157 def login_post(self):
158 158 c = self.load_default_context()
159 159
160 160 login_form = LoginForm(self.request.translate)()
161 161
162 162 try:
163 163 self.session.invalidate()
164 164 form_result = login_form.to_python(self.request.POST)
165 165 # form checks for username/password, now we're authenticated
166 166 headers = _store_user_in_session(
167 167 self.session,
168 168 username=form_result['username'],
169 169 remember=form_result['remember'])
170 170 log.debug('Redirecting to "%s" after login.', c.came_from)
171 171
172 172 audit_user = audit_logger.UserWrap(
173 173 username=self.request.POST.get('username'),
174 174 ip_addr=self.request.remote_addr)
175 175 action_data = {'user_agent': self.request.user_agent}
176 176 audit_logger.store_web(
177 177 'user.login.success', action_data=action_data,
178 178 user=audit_user, commit=True)
179 179
180 180 raise HTTPFound(c.came_from, headers=headers)
181 181 except formencode.Invalid as errors:
182 182 defaults = errors.value
183 183 # remove password from filling in form again
184 184 defaults.pop('password', None)
185 185 render_ctx = {
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.POST.get('username'),
192 192 ip_addr=self.request.remote_addr)
193 193 action_data = {'user_agent': self.request.user_agent}
194 194 audit_logger.store_web(
195 195 'user.login.failure', action_data=action_data,
196 196 user=audit_user, commit=True)
197 197 return self._get_template_context(c, **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 self.session.flash(e, queue='error')
204 h.flash(e, category='error')
205 205 return self._get_template_context(c)
206 206
207 207 @CSRFRequired()
208 208 @view_config(route_name='logout', request_method='POST')
209 209 def logout(self):
210 210 auth_user = self._rhodecode_user
211 211 log.info('Deleting session for user: `%s`', auth_user)
212 212
213 213 action_data = {'user_agent': self.request.user_agent}
214 214 audit_logger.store_web(
215 215 'user.logout', action_data=action_data,
216 216 user=auth_user, commit=True)
217 217 self.session.delete()
218 218 return HTTPFound(h.route_path('home'))
219 219
220 220 @HasPermissionAnyDecorator(
221 221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 222 @view_config(
223 223 route_name='register', request_method='GET',
224 224 renderer='rhodecode:templates/register.mako',)
225 225 def register(self, defaults=None, errors=None):
226 226 c = self.load_default_context()
227 227 defaults = defaults or {}
228 228 errors = errors or {}
229 229
230 230 settings = SettingsModel().get_all_settings()
231 231 register_message = settings.get('rhodecode_register_message') or ''
232 232 captcha = self._get_captcha_data()
233 233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 234 .AuthUser().permissions['global']
235 235
236 236 render_ctx = self._get_template_context(c)
237 237 render_ctx.update({
238 238 'defaults': defaults,
239 239 'errors': errors,
240 240 'auto_active': auto_active,
241 241 'captcha_active': captcha.active,
242 242 'captcha_public_key': captcha.public_key,
243 243 'register_message': register_message,
244 244 })
245 245 return render_ctx
246 246
247 247 @HasPermissionAnyDecorator(
248 248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 249 @view_config(
250 250 route_name='register', request_method='POST',
251 251 renderer='rhodecode:templates/register.mako')
252 252 def register_post(self):
253 self.load_default_context()
253 254 captcha = self._get_captcha_data()
254 255 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 256 .AuthUser().permissions['global']
256 257
257 258 register_form = RegisterForm(self.request.translate)()
258 259 try:
259 260
260 261 form_result = register_form.to_python(self.request.POST)
261 262 form_result['active'] = auto_active
262 263
263 264 if captcha.active:
264 265 response = submit(
265 266 self.request.POST.get('recaptcha_challenge_field'),
266 267 self.request.POST.get('recaptcha_response_field'),
267 268 private_key=captcha.private_key,
268 269 remoteip=get_ip_addr(self.request.environ))
269 270 if not response.is_valid:
270 271 _value = form_result
271 272 _msg = _('Bad captcha')
272 273 error_dict = {'recaptcha_field': _msg}
273 274 raise formencode.Invalid(_msg, _value, None,
274 275 error_dict=error_dict)
275 276
276 277 new_user = UserModel().create_registration(form_result)
277 278 event = UserRegistered(user=new_user, session=self.session)
278 self.request.registry.notify(event)
279 self.session.flash(
279 trigger(event)
280 h.flash(
280 281 _('You have successfully registered with RhodeCode'),
281 queue='success')
282 category='success')
282 283 Session().commit()
283 284
284 285 redirect_ro = self.request.route_path('login')
285 286 raise HTTPFound(redirect_ro)
286 287
287 288 except formencode.Invalid as errors:
288 289 errors.value.pop('password', None)
289 290 errors.value.pop('password_confirmation', None)
290 291 return self.register(
291 292 defaults=errors.value, errors=errors.error_dict)
292 293
293 294 except UserCreationError as e:
294 295 # container auth or other auth functions that create users on
295 296 # the fly can throw this exception signaling that there's issue
296 297 # with user creation, explanation should be provided in
297 298 # Exception itself
298 self.session.flash(e, queue='error')
299 h.flash(e, category='error')
299 300 return self.register()
300 301
301 302 @view_config(
302 303 route_name='reset_password', request_method=('GET', 'POST'),
303 304 renderer='rhodecode:templates/password_reset.mako')
304 305 def password_reset(self):
306 c = self.load_default_context()
305 307 captcha = self._get_captcha_data()
306 308
307 render_ctx = {
309 template_context = {
308 310 'captcha_active': captcha.active,
309 311 'captcha_public_key': captcha.public_key,
310 312 'defaults': {},
311 313 'errors': {},
312 314 }
313 315
314 316 # always send implicit message to prevent from discovery of
315 317 # matching emails
316 318 msg = _('If such email exists, a password reset link was sent to it.')
317 319
318 320 if self.request.POST:
319 321 if h.HasPermissionAny('hg.password_reset.disabled')():
320 322 _email = self.request.POST.get('email', '')
321 323 log.error('Failed attempt to reset password for `%s`.', _email)
322 self.session.flash(_('Password reset has been disabled.'),
323 queue='error')
324 h.flash(_('Password reset has been disabled.'),
325 category='error')
324 326 return HTTPFound(self.request.route_path('reset_password'))
325 327
326 328 password_reset_form = PasswordResetForm(self.request.translate)()
327 329 try:
328 330 form_result = password_reset_form.to_python(
329 331 self.request.POST)
330 332 user_email = form_result['email']
331 333
332 334 if captcha.active:
333 335 response = submit(
334 336 self.request.POST.get('recaptcha_challenge_field'),
335 337 self.request.POST.get('recaptcha_response_field'),
336 338 private_key=captcha.private_key,
337 339 remoteip=get_ip_addr(self.request.environ))
338 340 if not response.is_valid:
339 341 _value = form_result
340 342 _msg = _('Bad captcha')
341 343 error_dict = {'recaptcha_field': _msg}
342 344 raise formencode.Invalid(
343 345 _msg, _value, None, error_dict=error_dict)
344 346
345 347 # Generate reset URL and send mail.
346 348 user = User.get_by_email(user_email)
347 349
348 350 # generate password reset token that expires in 10minutes
349 351 desc = 'Generated token for password reset from {}'.format(
350 352 datetime.datetime.now().isoformat())
351 353 reset_token = AuthTokenModel().create(
352 354 user, lifetime=10,
353 355 description=desc,
354 356 role=UserApiKeys.ROLE_PASSWORD_RESET)
355 357 Session().commit()
356 358
357 359 log.debug('Successfully created password recovery token')
358 360 password_reset_url = self.request.route_url(
359 361 'reset_password_confirmation',
360 362 _query={'key': reset_token.api_key})
361 363 UserModel().reset_password_link(
362 364 form_result, password_reset_url)
363 365 # Display success message and redirect.
364 self.session.flash(msg, queue='success')
366 h.flash(msg, category='success')
365 367
366 368 action_data = {'email': user_email,
367 369 'user_agent': self.request.user_agent}
368 370 audit_logger.store_web(
369 371 'user.password.reset_request', action_data=action_data,
370 372 user=self._rhodecode_user, commit=True)
371 373 return HTTPFound(self.request.route_path('reset_password'))
372 374
373 375 except formencode.Invalid as errors:
374 render_ctx.update({
376 template_context.update({
375 377 'defaults': errors.value,
376 378 'errors': errors.error_dict,
377 379 })
378 380 if not self.request.POST.get('email'):
379 381 # case of empty email, we want to report that
380 return render_ctx
382 return self._get_template_context(c, **template_context)
381 383
382 384 if 'recaptcha_field' in errors.error_dict:
383 385 # case of failed captcha
384 return render_ctx
386 return self._get_template_context(c, **template_context)
385 387
386 388 log.debug('faking response on invalid password reset')
387 389 # make this take 2s, to prevent brute forcing.
388 390 time.sleep(2)
389 self.session.flash(msg, queue='success')
391 h.flash(msg, category='success')
390 392 return HTTPFound(self.request.route_path('reset_password'))
391 393
392 return render_ctx
394 return self._get_template_context(c, **template_context)
393 395
394 396 @view_config(route_name='reset_password_confirmation',
395 397 request_method='GET')
396 398 def password_reset_confirmation(self):
397
399 self.load_default_context()
398 400 if self.request.GET and self.request.GET.get('key'):
399 401 # make this take 2s, to prevent brute forcing.
400 402 time.sleep(2)
401 403
402 404 token = AuthTokenModel().get_auth_token(
403 405 self.request.GET.get('key'))
404 406
405 407 # verify token is the correct role
406 408 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
407 409 log.debug('Got token with role:%s expected is %s',
408 410 getattr(token, 'role', 'EMPTY_TOKEN'),
409 411 UserApiKeys.ROLE_PASSWORD_RESET)
410 self.session.flash(
411 _('Given reset token is invalid'), queue='error')
412 h.flash(
413 _('Given reset token is invalid'), category='error')
412 414 return HTTPFound(self.request.route_path('reset_password'))
413 415
414 416 try:
415 417 owner = token.user
416 418 data = {'email': owner.email, 'token': token.api_key}
417 419 UserModel().reset_password(data)
418 self.session.flash(
420 h.flash(
419 421 _('Your password reset was successful, '
420 422 'a new password has been sent to your email'),
421 queue='success')
423 category='success')
422 424 except Exception as e:
423 425 log.error(e)
424 426 return HTTPFound(self.request.route_path('reset_password'))
425 427
426 428 return HTTPFound(self.request.route_path('login'))
@@ -1,203 +1,203 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 # -*- coding: utf-8 -*-
21 21
22 22 # Copyright (C) 2016-2017 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
26 26 # (only), as published by the Free Software Foundation.
27 27 #
28 28 # This program is distributed in the hope that it will be useful,
29 29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 31 # GNU General Public License for more details.
32 32 #
33 33 # You should have received a copy of the GNU Affero General Public License
34 34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 35 #
36 36 # This program is dual-licensed. If you wish to learn more about the
37 37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39 39
40 40 import pytest
41 41
42 42 from rhodecode.model.db import User
43 43 from rhodecode.tests import TestController, assert_session_flash
44 44 from rhodecode.lib import helpers as h
45 45
46 46
47 47 def route_path(name, params=None, **kwargs):
48 48 import urllib
49 49 from rhodecode.apps._base import ADMIN_PREFIX
50 50
51 51 base_url = {
52 52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 56 }[name].format(**kwargs)
57 57
58 58 if params:
59 59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 60 return base_url
61 61
62 62
63 63 class TestMyAccountEdit(TestController):
64 64
65 65 def test_my_account_edit(self):
66 66 self.log_user()
67 67 response = self.app.get(route_path('my_account_edit'))
68 68
69 69 response.mustcontain('value="test_admin')
70 70
71 71 @pytest.mark.backends("git", "hg")
72 72 def test_my_account_my_pullrequests(self, pr_util):
73 73 self.log_user()
74 74 response = self.app.get(route_path('my_account_pullrequests'))
75 75 response.mustcontain('There are currently no open pull '
76 76 'requests requiring your participation.')
77 77
78 78 @pytest.mark.backends("git", "hg")
79 79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
80 80 self.log_user()
81 81 response = self.app.get(route_path('my_account_pullrequests_data'),
82 82 extra_environ=xhr_header)
83 83 assert response.json == {
84 84 u'data': [], u'draw': None,
85 85 u'recordsFiltered': 0, u'recordsTotal': 0}
86 86
87 87 pr = pr_util.create_pull_request(title='TestMyAccountPR')
88 88 expected = {
89 89 'author_raw': 'RhodeCode Admin',
90 90 'name_raw': pr.pull_request_id
91 91 }
92 92 response = self.app.get(route_path('my_account_pullrequests_data'),
93 93 extra_environ=xhr_header)
94 94 assert response.json['recordsTotal'] == 1
95 95 assert response.json['data'][0]['author_raw'] == expected['author_raw']
96 96
97 97 assert response.json['data'][0]['author_raw'] == expected['author_raw']
98 98 assert response.json['data'][0]['name_raw'] == expected['name_raw']
99 99
100 100 @pytest.mark.parametrize(
101 101 "name, attrs", [
102 102 ('firstname', {'firstname': 'new_username'}),
103 103 ('lastname', {'lastname': 'new_username'}),
104 104 ('admin', {'admin': True}),
105 105 ('admin', {'admin': False}),
106 106 ('extern_type', {'extern_type': 'ldap'}),
107 107 ('extern_type', {'extern_type': None}),
108 108 # ('extern_name', {'extern_name': 'test'}),
109 109 # ('extern_name', {'extern_name': None}),
110 110 ('active', {'active': False}),
111 111 ('active', {'active': True}),
112 112 ('email', {'email': 'some@email.com'}),
113 113 ])
114 114 def test_my_account_update(self, name, attrs, user_util):
115 115 usr = user_util.create_user(password='qweqwe')
116 116 params = usr.get_api_data() # current user data
117 117 user_id = usr.user_id
118 118 self.log_user(
119 119 username=usr.username, password='qweqwe')
120 120
121 121 params.update({'password_confirmation': ''})
122 122 params.update({'new_password': ''})
123 123 params.update({'extern_type': 'rhodecode'})
124 124 params.update({'extern_name': 'rhodecode'})
125 125 params.update({'csrf_token': self.csrf_token})
126 126
127 127 params.update(attrs)
128 128 # my account page cannot set language param yet, only for admins
129 129 del params['language']
130 130 response = self.app.post(route_path('my_account_update'), params)
131 131
132 132 assert_session_flash(
133 133 response, 'Your account was updated successfully')
134 134
135 135 del params['csrf_token']
136 136
137 137 updated_user = User.get(user_id)
138 138 updated_params = updated_user.get_api_data()
139 139 updated_params.update({'password_confirmation': ''})
140 140 updated_params.update({'new_password': ''})
141 141
142 142 params['last_login'] = updated_params['last_login']
143 143 params['last_activity'] = updated_params['last_activity']
144 144 # my account page cannot set language param yet, only for admins
145 145 # but we get this info from API anyway
146 146 params['language'] = updated_params['language']
147 147
148 148 if name == 'email':
149 149 params['emails'] = [attrs['email']]
150 150 if name == 'extern_type':
151 151 # cannot update this via form, expected value is original one
152 152 params['extern_type'] = "rhodecode"
153 153 if name == 'extern_name':
154 154 # cannot update this via form, expected value is original one
155 155 params['extern_name'] = str(user_id)
156 156 if name == 'active':
157 157 # my account cannot deactivate account
158 158 params['active'] = True
159 159 if name == 'admin':
160 160 # my account cannot make you an admin !
161 161 params['admin'] = False
162 162
163 163 assert params == updated_params
164 164
165 165 def test_my_account_update_err_email_exists(self):
166 166 self.log_user()
167 167
168 168 new_email = 'test_regular@mail.com' # already existing email
169 169 params = {
170 170 'username': 'test_admin',
171 171 'new_password': 'test12',
172 172 'password_confirmation': 'test122',
173 173 'firstname': 'NewName',
174 174 'lastname': 'NewLastname',
175 175 'email': new_email,
176 176 'csrf_token': self.csrf_token,
177 177 }
178 178
179 179 response = self.app.post(route_path('my_account_update'),
180 180 params=params)
181 181
182 182 response.mustcontain('This e-mail address is already taken')
183 183
184 184 def test_my_account_update_bad_email_address(self):
185 185 self.log_user('test_regular2', 'test12')
186 186
187 187 new_email = 'newmail.pl'
188 188 params = {
189 189 'username': 'test_admin',
190 190 'new_password': 'test12',
191 191 'password_confirmation': 'test122',
192 192 'firstname': 'NewName',
193 193 'lastname': 'NewLastname',
194 194 'email': new_email,
195 195 'csrf_token': self.csrf_token,
196 196 }
197 197 response = self.app.post(route_path('my_account_update'),
198 198 params=params)
199 199
200 200 response.mustcontain('An email address must contain a single @')
201 msg = '???'
201 msg = u'Username "%(username)s" already exists'
202 202 msg = h.html_escape(msg % {'username': 'test_admin'})
203 203 response.mustcontain(u"%s" % msg)
@@ -1,584 +1,585 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
37 37 from rhodecode.lib.channelstream import (
38 38 channelstream_request, ChannelstreamException)
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 44 PullRequest)
45 45 from rhodecode.model.forms import UserForm, UserExtraEmailForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.scm import RepoList
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.validation_schema.schemas import user_schema
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class MyAccountView(BaseAppView, DataGridAppView):
57 57 ALLOW_SCOPED_TOKENS = False
58 58 """
59 59 This view has alternative version inside EE, if modified please take a look
60 60 in there as well.
61 61 """
62 62
63 63 def load_default_context(self):
64 64 c = self._get_local_tmpl_context()
65 65 c.user = c.auth_user.get_instance()
66 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 67
68 68 return c
69 69
70 70 @LoginRequired()
71 71 @NotAnonymous()
72 72 @view_config(
73 73 route_name='my_account_profile', request_method='GET',
74 74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 75 def my_account_profile(self):
76 76 c = self.load_default_context()
77 77 c.active = 'profile'
78 78 return self._get_template_context(c)
79 79
80 80 @LoginRequired()
81 81 @NotAnonymous()
82 82 @view_config(
83 83 route_name='my_account_password', request_method='GET',
84 84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 85 def my_account_password(self):
86 86 c = self.load_default_context()
87 87 c.active = 'password'
88 88 c.extern_type = c.user.extern_type
89 89
90 90 schema = user_schema.ChangePasswordSchema().bind(
91 91 username=c.user.username)
92 92
93 93 form = forms.Form(
94 94 schema,
95 95 action=h.route_path('my_account_password_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 c.form = form
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @NotAnonymous()
103 103 @CSRFRequired()
104 104 @view_config(
105 105 route_name='my_account_password_update', request_method='POST',
106 106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
107 107 def my_account_password_update(self):
108 108 _ = self.request.translate
109 109 c = self.load_default_context()
110 110 c.active = 'password'
111 111 c.extern_type = c.user.extern_type
112 112
113 113 schema = user_schema.ChangePasswordSchema().bind(
114 114 username=c.user.username)
115 115
116 116 form = forms.Form(
117 117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
118 118
119 119 if c.extern_type != 'rhodecode':
120 120 raise HTTPFound(self.request.route_path('my_account_password'))
121 121
122 122 controls = self.request.POST.items()
123 123 try:
124 124 valid_data = form.validate(controls)
125 125 UserModel().update_user(c.user.user_id, **valid_data)
126 126 c.user.update_userdata(force_password_change=False)
127 127 Session().commit()
128 128 except forms.ValidationFailure as e:
129 129 c.form = e
130 130 return self._get_template_context(c)
131 131
132 132 except Exception:
133 133 log.exception("Exception updating password")
134 134 h.flash(_('Error occurred during update of user password'),
135 135 category='error')
136 136 else:
137 137 instance = c.auth_user.get_instance()
138 138 self.session.setdefault('rhodecode_user', {}).update(
139 139 {'password': md5(instance.password)})
140 140 self.session.save()
141 141 h.flash(_("Successfully updated password"), category='success')
142 142
143 143 raise HTTPFound(self.request.route_path('my_account_password'))
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @view_config(
148 148 route_name='my_account_auth_tokens', request_method='GET',
149 149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
150 150 def my_account_auth_tokens(self):
151 151 _ = self.request.translate
152 152
153 153 c = self.load_default_context()
154 154 c.active = 'auth_tokens'
155 155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
156 156 c.role_values = [
157 157 (x, AuthTokenModel.cls._get_role_name(x))
158 158 for x in AuthTokenModel.cls.ROLES]
159 159 c.role_options = [(c.role_values, _("Role"))]
160 160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
161 161 c.user.user_id, show_expired=True)
162 162 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
163 163 return self._get_template_context(c)
164 164
165 165 def maybe_attach_token_scope(self, token):
166 166 # implemented in EE edition
167 167 pass
168 168
169 169 @LoginRequired()
170 170 @NotAnonymous()
171 171 @CSRFRequired()
172 172 @view_config(
173 173 route_name='my_account_auth_tokens_add', request_method='POST',)
174 174 def my_account_auth_tokens_add(self):
175 175 _ = self.request.translate
176 176 c = self.load_default_context()
177 177
178 178 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
179 179 description = self.request.POST.get('description')
180 180 role = self.request.POST.get('role')
181 181
182 182 token = AuthTokenModel().create(
183 183 c.user.user_id, description, lifetime, role)
184 184 token_data = token.get_api_data()
185 185
186 186 self.maybe_attach_token_scope(token)
187 187 audit_logger.store_web(
188 188 'user.edit.token.add', action_data={
189 189 'data': {'token': token_data, 'user': 'self'}},
190 190 user=self._rhodecode_user, )
191 191 Session().commit()
192 192
193 193 h.flash(_("Auth token successfully created"), category='success')
194 194 return HTTPFound(h.route_path('my_account_auth_tokens'))
195 195
196 196 @LoginRequired()
197 197 @NotAnonymous()
198 198 @CSRFRequired()
199 199 @view_config(
200 200 route_name='my_account_auth_tokens_delete', request_method='POST')
201 201 def my_account_auth_tokens_delete(self):
202 202 _ = self.request.translate
203 203 c = self.load_default_context()
204 204
205 205 del_auth_token = self.request.POST.get('del_auth_token')
206 206
207 207 if del_auth_token:
208 208 token = UserApiKeys.get_or_404(del_auth_token)
209 209 token_data = token.get_api_data()
210 210
211 211 AuthTokenModel().delete(del_auth_token, c.user.user_id)
212 212 audit_logger.store_web(
213 213 'user.edit.token.delete', action_data={
214 214 'data': {'token': token_data, 'user': 'self'}},
215 215 user=self._rhodecode_user,)
216 216 Session().commit()
217 217 h.flash(_("Auth token successfully deleted"), category='success')
218 218
219 219 return HTTPFound(h.route_path('my_account_auth_tokens'))
220 220
221 221 @LoginRequired()
222 222 @NotAnonymous()
223 223 @view_config(
224 224 route_name='my_account_emails', request_method='GET',
225 225 renderer='rhodecode:templates/admin/my_account/my_account.mako')
226 226 def my_account_emails(self):
227 227 _ = self.request.translate
228 228
229 229 c = self.load_default_context()
230 230 c.active = 'emails'
231 231
232 232 c.user_email_map = UserEmailMap.query()\
233 233 .filter(UserEmailMap.user == c.user).all()
234 234 return self._get_template_context(c)
235 235
236 236 @LoginRequired()
237 237 @NotAnonymous()
238 238 @CSRFRequired()
239 239 @view_config(
240 240 route_name='my_account_emails_add', request_method='POST')
241 241 def my_account_emails_add(self):
242 242 _ = self.request.translate
243 243 c = self.load_default_context()
244 244
245 245 email = self.request.POST.get('new_email')
246 246
247 247 try:
248 248 form = UserExtraEmailForm(self.request.translate)()
249 249 data = form.to_python({'email': email})
250 250 email = data['email']
251 251
252 252 UserModel().add_extra_email(c.user.user_id, email)
253 253 audit_logger.store_web(
254 254 'user.edit.email.add', action_data={
255 255 'data': {'email': email, 'user': 'self'}},
256 256 user=self._rhodecode_user,)
257 257
258 258 Session().commit()
259 259 h.flash(_("Added new email address `%s` for user account") % email,
260 260 category='success')
261 261 except formencode.Invalid as error:
262 262 h.flash(h.escape(error.error_dict['email']), category='error')
263 263 except Exception:
264 264 log.exception("Exception in my_account_emails")
265 265 h.flash(_('An error occurred during email saving'),
266 266 category='error')
267 267 return HTTPFound(h.route_path('my_account_emails'))
268 268
269 269 @LoginRequired()
270 270 @NotAnonymous()
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='my_account_emails_delete', request_method='POST')
274 274 def my_account_emails_delete(self):
275 275 _ = self.request.translate
276 276 c = self.load_default_context()
277 277
278 278 del_email_id = self.request.POST.get('del_email_id')
279 279 if del_email_id:
280 280 email = UserEmailMap.get_or_404(del_email_id).email
281 281 UserModel().delete_extra_email(c.user.user_id, del_email_id)
282 282 audit_logger.store_web(
283 283 'user.edit.email.delete', action_data={
284 284 'data': {'email': email, 'user': 'self'}},
285 285 user=self._rhodecode_user,)
286 286 Session().commit()
287 287 h.flash(_("Email successfully deleted"),
288 288 category='success')
289 289 return HTTPFound(h.route_path('my_account_emails'))
290 290
291 291 @LoginRequired()
292 292 @NotAnonymous()
293 293 @CSRFRequired()
294 294 @view_config(
295 295 route_name='my_account_notifications_test_channelstream',
296 296 request_method='POST', renderer='json_ext')
297 297 def my_account_notifications_test_channelstream(self):
298 298 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
299 299 self._rhodecode_user.username, datetime.datetime.now())
300 300 payload = {
301 301 # 'channel': 'broadcast',
302 302 'type': 'message',
303 303 'timestamp': datetime.datetime.utcnow(),
304 304 'user': 'system',
305 305 'pm_users': [self._rhodecode_user.username],
306 306 'message': {
307 307 'message': message,
308 308 'level': 'info',
309 309 'topic': '/notifications'
310 310 }
311 311 }
312 312
313 313 registry = self.request.registry
314 314 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
315 315 channelstream_config = rhodecode_plugins.get('channelstream', {})
316 316
317 317 try:
318 318 channelstream_request(channelstream_config, [payload], '/message')
319 319 except ChannelstreamException as e:
320 320 log.exception('Failed to send channelstream data')
321 321 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
322 322 return {"response": 'Channelstream data sent. '
323 323 'You should see a new live message now.'}
324 324
325 325 def _load_my_repos_data(self, watched=False):
326 326 if watched:
327 327 admin = False
328 328 follows_repos = Session().query(UserFollowing)\
329 329 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
330 330 .options(joinedload(UserFollowing.follows_repository))\
331 331 .all()
332 332 repo_list = [x.follows_repository for x in follows_repos]
333 333 else:
334 334 admin = True
335 335 repo_list = Repository.get_all_repos(
336 336 user_id=self._rhodecode_user.user_id)
337 337 repo_list = RepoList(repo_list, perm_set=[
338 338 'repository.read', 'repository.write', 'repository.admin'])
339 339
340 340 repos_data = RepoModel().get_repos_as_dict(
341 341 repo_list=repo_list, admin=admin)
342 342 # json used to render the grid
343 343 return json.dumps(repos_data)
344 344
345 345 @LoginRequired()
346 346 @NotAnonymous()
347 347 @view_config(
348 348 route_name='my_account_repos', request_method='GET',
349 349 renderer='rhodecode:templates/admin/my_account/my_account.mako')
350 350 def my_account_repos(self):
351 351 c = self.load_default_context()
352 352 c.active = 'repos'
353 353
354 354 # json used to render the grid
355 355 c.data = self._load_my_repos_data()
356 356 return self._get_template_context(c)
357 357
358 358 @LoginRequired()
359 359 @NotAnonymous()
360 360 @view_config(
361 361 route_name='my_account_watched', request_method='GET',
362 362 renderer='rhodecode:templates/admin/my_account/my_account.mako')
363 363 def my_account_watched(self):
364 364 c = self.load_default_context()
365 365 c.active = 'watched'
366 366
367 367 # json used to render the grid
368 368 c.data = self._load_my_repos_data(watched=True)
369 369 return self._get_template_context(c)
370 370
371 371 @LoginRequired()
372 372 @NotAnonymous()
373 373 @view_config(
374 374 route_name='my_account_perms', request_method='GET',
375 375 renderer='rhodecode:templates/admin/my_account/my_account.mako')
376 376 def my_account_perms(self):
377 377 c = self.load_default_context()
378 378 c.active = 'perms'
379 379
380 380 c.perm_user = c.auth_user
381 381 return self._get_template_context(c)
382 382
383 383 @LoginRequired()
384 384 @NotAnonymous()
385 385 @view_config(
386 386 route_name='my_account_notifications', request_method='GET',
387 387 renderer='rhodecode:templates/admin/my_account/my_account.mako')
388 388 def my_notifications(self):
389 389 c = self.load_default_context()
390 390 c.active = 'notifications'
391 391
392 392 return self._get_template_context(c)
393 393
394 394 @LoginRequired()
395 395 @NotAnonymous()
396 396 @CSRFRequired()
397 397 @view_config(
398 398 route_name='my_account_notifications_toggle_visibility',
399 399 request_method='POST', renderer='json_ext')
400 400 def my_notifications_toggle_visibility(self):
401 401 user = self._rhodecode_db_user
402 402 new_status = not user.user_data.get('notification_status', True)
403 403 user.update_userdata(notification_status=new_status)
404 404 Session().commit()
405 405 return user.user_data['notification_status']
406 406
407 407 @LoginRequired()
408 408 @NotAnonymous()
409 409 @view_config(
410 410 route_name='my_account_edit',
411 411 request_method='GET',
412 412 renderer='rhodecode:templates/admin/my_account/my_account.mako')
413 413 def my_account_edit(self):
414 414 c = self.load_default_context()
415 415 c.active = 'profile_edit'
416 416
417 417 c.perm_user = c.auth_user
418 418 c.extern_type = c.user.extern_type
419 419 c.extern_name = c.user.extern_name
420 420
421 421 defaults = c.user.get_dict()
422 422
423 423 data = render('rhodecode:templates/admin/my_account/my_account.mako',
424 424 self._get_template_context(c), self.request)
425 425 html = formencode.htmlfill.render(
426 426 data,
427 427 defaults=defaults,
428 428 encoding="UTF-8",
429 429 force_defaults=False
430 430 )
431 431 return Response(html)
432 432
433 433 @LoginRequired()
434 434 @NotAnonymous()
435 435 @CSRFRequired()
436 436 @view_config(
437 437 route_name='my_account_update',
438 438 request_method='POST',
439 439 renderer='rhodecode:templates/admin/my_account/my_account.mako')
440 440 def my_account_update(self):
441 441 _ = self.request.translate
442 442 c = self.load_default_context()
443 443 c.active = 'profile_edit'
444 444
445 445 c.perm_user = c.auth_user
446 446 c.extern_type = c.user.extern_type
447 447 c.extern_name = c.user.extern_name
448 448
449 449 _form = UserForm(self.request.translate, edit=True,
450 450 old_data={'user_id': self._rhodecode_user.user_id,
451 451 'email': self._rhodecode_user.email})()
452 452 form_result = {}
453 453 try:
454 454 post_data = dict(self.request.POST)
455 455 post_data['new_password'] = ''
456 456 post_data['password_confirmation'] = ''
457 457 form_result = _form.to_python(post_data)
458 458 # skip updating those attrs for my account
459 459 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
460 460 'new_password', 'password_confirmation']
461 461 # TODO: plugin should define if username can be updated
462 462 if c.extern_type != "rhodecode":
463 463 # forbid updating username for external accounts
464 464 skip_attrs.append('username')
465 465
466 466 UserModel().update_user(
467 467 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
468 468 **form_result)
469 469 h.flash(_('Your account was updated successfully'),
470 470 category='success')
471 471 Session().commit()
472 472
473 473 except formencode.Invalid as errors:
474 474 data = render(
475 475 'rhodecode:templates/admin/my_account/my_account.mako',
476 476 self._get_template_context(c), self.request)
477 477
478 478 html = formencode.htmlfill.render(
479 479 data,
480 480 defaults=errors.value,
481 481 errors=errors.error_dict or {},
482 482 prefix_error=False,
483 483 encoding="UTF-8",
484 484 force_defaults=False)
485 485 return Response(html)
486 486
487 487 except Exception:
488 488 log.exception("Exception updating user")
489 489 h.flash(_('Error occurred during update of user %s')
490 490 % form_result.get('username'), category='error')
491 491 raise HTTPFound(h.route_path('my_account_profile'))
492 492
493 493 raise HTTPFound(h.route_path('my_account_profile'))
494 494
495 495 def _get_pull_requests_list(self, statuses):
496 496 draw, start, limit = self._extract_chunk(self.request)
497 497 search_q, order_by, order_dir = self._extract_ordering(self.request)
498 498 _render = self.request.get_partial_renderer(
499 499 'rhodecode:templates/data_table/_dt_elements.mako')
500 500
501 501 pull_requests = PullRequestModel().get_im_participating_in(
502 502 user_id=self._rhodecode_user.user_id,
503 503 statuses=statuses,
504 504 offset=start, length=limit, order_by=order_by,
505 505 order_dir=order_dir)
506 506
507 507 pull_requests_total_count = PullRequestModel().count_im_participating_in(
508 508 user_id=self._rhodecode_user.user_id, statuses=statuses)
509 509
510 510 data = []
511 511 comments_model = CommentsModel()
512 512 for pr in pull_requests:
513 513 repo_id = pr.target_repo_id
514 514 comments = comments_model.get_all_comments(
515 515 repo_id, pull_request=pr)
516 516 owned = pr.user_id == self._rhodecode_user.user_id
517 517
518 518 data.append({
519 519 'target_repo': _render('pullrequest_target_repo',
520 520 pr.target_repo.repo_name),
521 521 'name': _render('pullrequest_name',
522 522 pr.pull_request_id, pr.target_repo.repo_name,
523 523 short=True),
524 524 'name_raw': pr.pull_request_id,
525 525 'status': _render('pullrequest_status',
526 526 pr.calculated_review_status()),
527 527 'title': _render(
528 528 'pullrequest_title', pr.title, pr.description),
529 529 'description': h.escape(pr.description),
530 530 'updated_on': _render('pullrequest_updated_on',
531 531 h.datetime_to_time(pr.updated_on)),
532 532 'updated_on_raw': h.datetime_to_time(pr.updated_on),
533 533 'created_on': _render('pullrequest_updated_on',
534 534 h.datetime_to_time(pr.created_on)),
535 535 'created_on_raw': h.datetime_to_time(pr.created_on),
536 536 'author': _render('pullrequest_author',
537 537 pr.author.full_contact, ),
538 538 'author_raw': pr.author.full_name,
539 539 'comments': _render('pullrequest_comments', len(comments)),
540 540 'comments_raw': len(comments),
541 541 'closed': pr.is_closed(),
542 542 'owned': owned
543 543 })
544 544
545 545 # json used to render the grid
546 546 data = ({
547 547 'draw': draw,
548 548 'data': data,
549 549 'recordsTotal': pull_requests_total_count,
550 550 'recordsFiltered': pull_requests_total_count,
551 551 })
552 552 return data
553 553
554 554 @LoginRequired()
555 555 @NotAnonymous()
556 556 @view_config(
557 557 route_name='my_account_pullrequests',
558 558 request_method='GET',
559 559 renderer='rhodecode:templates/admin/my_account/my_account.mako')
560 560 def my_account_pullrequests(self):
561 561 c = self.load_default_context()
562 562 c.active = 'pullrequests'
563 563 req_get = self.request.GET
564 564
565 565 c.closed = str2bool(req_get.get('pr_show_closed'))
566 566
567 567 return self._get_template_context(c)
568 568
569 569 @LoginRequired()
570 570 @NotAnonymous()
571 571 @view_config(
572 572 route_name='my_account_pullrequests_data',
573 573 request_method='GET', renderer='json_ext')
574 574 def my_account_pullrequests_data(self):
575 self.load_default_context()
575 576 req_get = self.request.GET
576 577 closed = str2bool(req_get.get('closed'))
577 578
578 579 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
579 580 if closed:
580 581 statuses += [PullRequest.STATUS_CLOSED]
581 582
582 583 data = self._get_pull_requests_list(statuses=statuses)
583 584 return data
584 585
@@ -1,1064 +1,1068 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.vcs import nodes
31 31
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.tests import assert_session_flash
34 34 from rhodecode.tests.fixture import Fixture
35 35
36 36 fixture = Fixture()
37 37
38 38
39 39 def get_node_history(backend_type):
40 40 return {
41 41 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
42 42 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
43 43 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
44 44 }[backend_type]
45 45
46 46
47 47 def route_path(name, params=None, **kwargs):
48 48 import urllib
49 49
50 50 base_url = {
51 'repo_summary': '/{repo_name}',
51 52 'repo_archivefile': '/{repo_name}/archive/{fname}',
52 53 'repo_files_diff': '/{repo_name}/diff/{f_path}',
53 54 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
54 55 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
55 56 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
56 57 'repo_files:default_commit': '/{repo_name}/files',
57 58 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
58 59 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
59 60 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
60 61 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
61 62 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
62 63 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
63 64 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
64 65 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
65 66 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
66 67 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
67 68 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
68 69 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
69 70 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
70 71 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
71 72 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
72 73 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
73 74 }[name].format(**kwargs)
74 75
75 76 if params:
76 77 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
77 78 return base_url
78 79
79 80
80 81 def assert_files_in_response(response, files, params):
81 82 template = (
82 83 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
83 84 _assert_items_in_response(response, files, template, params)
84 85
85 86
86 87 def assert_dirs_in_response(response, dirs, params):
87 88 template = (
88 89 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
89 90 _assert_items_in_response(response, dirs, template, params)
90 91
91 92
92 93 def _assert_items_in_response(response, items, template, params):
93 94 for item in items:
94 95 item_params = {'name': item}
95 96 item_params.update(params)
96 97 response.mustcontain(template % item_params)
97 98
98 99
99 100 def assert_timeago_in_response(response, items, params):
100 101 for item in items:
101 102 response.mustcontain(h.age_component(params['date']))
102 103
103 104
104 105 @pytest.mark.usefixtures("app")
105 106 class TestFilesViews(object):
106 107
107 108 def test_show_files(self, backend):
108 109 response = self.app.get(
109 110 route_path('repo_files',
110 111 repo_name=backend.repo_name,
111 112 commit_id='tip', f_path='/'))
112 113 commit = backend.repo.get_commit()
113 114
114 115 params = {
115 116 'repo_name': backend.repo_name,
116 117 'commit_id': commit.raw_id,
117 118 'date': commit.date
118 119 }
119 120 assert_dirs_in_response(response, ['docs', 'vcs'], params)
120 121 files = [
121 122 '.gitignore',
122 123 '.hgignore',
123 124 '.hgtags',
124 125 # TODO: missing in Git
125 126 # '.travis.yml',
126 127 'MANIFEST.in',
127 128 'README.rst',
128 129 # TODO: File is missing in svn repository
129 130 # 'run_test_and_report.sh',
130 131 'setup.cfg',
131 132 'setup.py',
132 133 'test_and_report.sh',
133 134 'tox.ini',
134 135 ]
135 136 assert_files_in_response(response, files, params)
136 137 assert_timeago_in_response(response, files, params)
137 138
138 139 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
139 140 repo = backend_hg['subrepos']
140 141 response = self.app.get(
141 142 route_path('repo_files',
142 143 repo_name=repo.repo_name,
143 144 commit_id='tip', f_path='/'))
144 145 assert_response = response.assert_response()
145 146 assert_response.contains_one_link(
146 147 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
147 148
148 149 def test_show_files_links_submodules_with_absolute_url_subpaths(
149 150 self, backend_hg):
150 151 repo = backend_hg['subrepos']
151 152 response = self.app.get(
152 153 route_path('repo_files',
153 154 repo_name=repo.repo_name,
154 155 commit_id='tip', f_path='/'))
155 156 assert_response = response.assert_response()
156 157 assert_response.contains_one_link(
157 158 'subpaths-path @ 000000000000',
158 159 'http://sub-base.example.com/subpaths-path')
159 160
160 161 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
161 162 def test_files_menu(self, backend):
162 163 new_branch = "temp_branch_name"
163 164 commits = [
164 165 {'message': 'a'},
165 166 {'message': 'b', 'branch': new_branch}
166 167 ]
167 168 backend.create_repo(commits)
168 169
169 170 backend.repo.landing_rev = "branch:%s" % new_branch
170 171
171 172 # get response based on tip and not new commit
172 173 response = self.app.get(
173 174 route_path('repo_files',
174 175 repo_name=backend.repo_name,
175 176 commit_id='tip', f_path='/'))
176 177
177 178 # make sure Files menu url is not tip but new commit
178 179 landing_rev = backend.repo.landing_rev[1]
179 180 files_url = route_path('repo_files:default_path',
180 181 repo_name=backend.repo_name,
181 182 commit_id=landing_rev)
182 183
183 184 assert landing_rev != 'tip'
184 185 response.mustcontain(
185 186 '<li class="active"><a class="menulink" href="%s">' % files_url)
186 187
187 188 def test_show_files_commit(self, backend):
188 189 commit = backend.repo.get_commit(commit_idx=32)
189 190
190 191 response = self.app.get(
191 192 route_path('repo_files',
192 193 repo_name=backend.repo_name,
193 194 commit_id=commit.raw_id, f_path='/'))
194 195
195 196 dirs = ['docs', 'tests']
196 197 files = ['README.rst']
197 198 params = {
198 199 'repo_name': backend.repo_name,
199 200 'commit_id': commit.raw_id,
200 201 }
201 202 assert_dirs_in_response(response, dirs, params)
202 203 assert_files_in_response(response, files, params)
203 204
204 205 def test_show_files_different_branch(self, backend):
205 206 branches = dict(
206 207 hg=(150, ['git']),
207 208 # TODO: Git test repository does not contain other branches
208 209 git=(633, ['master']),
209 210 # TODO: Branch support in Subversion
210 211 svn=(150, [])
211 212 )
212 213 idx, branches = branches[backend.alias]
213 214 commit = backend.repo.get_commit(commit_idx=idx)
214 215 response = self.app.get(
215 216 route_path('repo_files',
216 217 repo_name=backend.repo_name,
217 218 commit_id=commit.raw_id, f_path='/'))
218 219
219 220 assert_response = response.assert_response()
220 221 for branch in branches:
221 222 assert_response.element_contains('.tags .branchtag', branch)
222 223
223 224 def test_show_files_paging(self, backend):
224 225 repo = backend.repo
225 226 indexes = [73, 92, 109, 1, 0]
226 227 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
227 228 for rev in indexes]
228 229
229 230 for idx in idx_map:
230 231 response = self.app.get(
231 232 route_path('repo_files',
232 233 repo_name=backend.repo_name,
233 234 commit_id=idx[1], f_path='/'))
234 235
235 236 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
236 237
237 238 def test_file_source(self, backend):
238 239 commit = backend.repo.get_commit(commit_idx=167)
239 240 response = self.app.get(
240 241 route_path('repo_files',
241 242 repo_name=backend.repo_name,
242 243 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
243 244
244 245 msgbox = """<div class="commit right-content">%s</div>"""
245 246 response.mustcontain(msgbox % (commit.message, ))
246 247
247 248 assert_response = response.assert_response()
248 249 if commit.branch:
249 250 assert_response.element_contains(
250 251 '.tags.tags-main .branchtag', commit.branch)
251 252 if commit.tags:
252 253 for tag in commit.tags:
253 254 assert_response.element_contains('.tags.tags-main .tagtag', tag)
254 255
255 256 def test_file_source_annotated(self, backend):
256 257 response = self.app.get(
257 258 route_path('repo_files:annotated',
258 259 repo_name=backend.repo_name,
259 260 commit_id='tip', f_path='vcs/nodes.py'))
260 261 expected_commits = {
261 262 'hg': 'r356',
262 263 'git': 'r345',
263 264 'svn': 'r208',
264 265 }
265 266 response.mustcontain(expected_commits[backend.alias])
266 267
267 268 def test_file_source_authors(self, backend):
268 269 response = self.app.get(
269 270 route_path('repo_file_authors',
270 271 repo_name=backend.repo_name,
271 272 commit_id='tip', f_path='vcs/nodes.py'))
272 273 expected_authors = {
273 274 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
274 275 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
275 276 'svn': ('marcin', 'lukasz'),
276 277 }
277 278
278 279 for author in expected_authors[backend.alias]:
279 280 response.mustcontain(author)
280 281
281 282 def test_file_source_authors_with_annotation(self, backend):
282 283 response = self.app.get(
283 284 route_path('repo_file_authors',
284 285 repo_name=backend.repo_name,
285 286 commit_id='tip', f_path='vcs/nodes.py',
286 287 params=dict(annotate=1)))
287 288 expected_authors = {
288 289 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
289 290 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
290 291 'svn': ('marcin', 'lukasz'),
291 292 }
292 293
293 294 for author in expected_authors[backend.alias]:
294 295 response.mustcontain(author)
295 296
296 297 def test_file_source_history(self, backend, xhr_header):
297 298 response = self.app.get(
298 299 route_path('repo_file_history',
299 300 repo_name=backend.repo_name,
300 301 commit_id='tip', f_path='vcs/nodes.py'),
301 302 extra_environ=xhr_header)
302 303 assert get_node_history(backend.alias) == json.loads(response.body)
303 304
304 305 def test_file_source_history_svn(self, backend_svn, xhr_header):
305 306 simple_repo = backend_svn['svn-simple-layout']
306 307 response = self.app.get(
307 308 route_path('repo_file_history',
308 309 repo_name=simple_repo.repo_name,
309 310 commit_id='tip', f_path='trunk/example.py'),
310 311 extra_environ=xhr_header)
311 312
312 313 expected_data = json.loads(
313 314 fixture.load_resource('svn_node_history_branches.json'))
314 315 assert expected_data == response.json
315 316
316 317 def test_file_source_history_with_annotation(self, backend, xhr_header):
317 318 response = self.app.get(
318 319 route_path('repo_file_history',
319 320 repo_name=backend.repo_name,
320 321 commit_id='tip', f_path='vcs/nodes.py',
321 322 params=dict(annotate=1)),
322 323
323 324 extra_environ=xhr_header)
324 325 assert get_node_history(backend.alias) == json.loads(response.body)
325 326
326 327 def test_tree_search_top_level(self, backend, xhr_header):
327 328 commit = backend.repo.get_commit(commit_idx=173)
328 329 response = self.app.get(
329 330 route_path('repo_files_nodelist',
330 331 repo_name=backend.repo_name,
331 332 commit_id=commit.raw_id, f_path='/'),
332 333 extra_environ=xhr_header)
333 334 assert 'nodes' in response.json
334 335 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
335 336
336 337 def test_tree_search_missing_xhr(self, backend):
337 338 self.app.get(
338 339 route_path('repo_files_nodelist',
339 340 repo_name=backend.repo_name,
340 341 commit_id='tip', f_path='/'),
341 342 status=404)
342 343
343 344 def test_tree_search_at_path(self, backend, xhr_header):
344 345 commit = backend.repo.get_commit(commit_idx=173)
345 346 response = self.app.get(
346 347 route_path('repo_files_nodelist',
347 348 repo_name=backend.repo_name,
348 349 commit_id=commit.raw_id, f_path='/docs'),
349 350 extra_environ=xhr_header)
350 351 assert 'nodes' in response.json
351 352 nodes = response.json['nodes']
352 353 assert {'name': 'docs/api', 'type': 'dir'} in nodes
353 354 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
354 355
355 356 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
356 357 commit = backend.repo.get_commit(commit_idx=173)
357 358 response = self.app.get(
358 359 route_path('repo_files_nodelist',
359 360 repo_name=backend.repo_name,
360 361 commit_id=commit.raw_id, f_path='/docs/api'),
361 362 extra_environ=xhr_header)
362 363 assert 'nodes' in response.json
363 364 nodes = response.json['nodes']
364 365 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
365 366
366 367 def test_tree_search_at_path_missing_xhr(self, backend):
367 368 self.app.get(
368 369 route_path('repo_files_nodelist',
369 370 repo_name=backend.repo_name,
370 371 commit_id='tip', f_path='/docs'),
371 372 status=404)
372 373
373 374 def test_nodetree(self, backend, xhr_header):
374 375 commit = backend.repo.get_commit(commit_idx=173)
375 376 response = self.app.get(
376 377 route_path('repo_nodetree_full',
377 378 repo_name=backend.repo_name,
378 379 commit_id=commit.raw_id, f_path='/'),
379 380 extra_environ=xhr_header)
380 381
381 382 assert_response = response.assert_response()
382 383
383 384 for attr in ['data-commit-id', 'data-date', 'data-author']:
384 385 elements = assert_response.get_elements('[{}]'.format(attr))
385 386 assert len(elements) > 1
386 387
387 388 for element in elements:
388 389 assert element.get(attr)
389 390
390 391 def test_nodetree_if_file(self, backend, xhr_header):
391 392 commit = backend.repo.get_commit(commit_idx=173)
392 393 response = self.app.get(
393 394 route_path('repo_nodetree_full',
394 395 repo_name=backend.repo_name,
395 396 commit_id=commit.raw_id, f_path='README.rst'),
396 397 extra_environ=xhr_header)
397 398 assert response.body == ''
398 399
399 400 def test_nodetree_wrong_path(self, backend, xhr_header):
400 401 commit = backend.repo.get_commit(commit_idx=173)
401 402 response = self.app.get(
402 403 route_path('repo_nodetree_full',
403 404 repo_name=backend.repo_name,
404 405 commit_id=commit.raw_id, f_path='/dont-exist'),
405 406 extra_environ=xhr_header)
406 407
407 408 err = 'error: There is no file nor ' \
408 409 'directory at the given path'
409 410 assert err in response.body
410 411
411 412 def test_nodetree_missing_xhr(self, backend):
412 413 self.app.get(
413 414 route_path('repo_nodetree_full',
414 415 repo_name=backend.repo_name,
415 416 commit_id='tip', f_path='/'),
416 417 status=404)
417 418
418 419
419 420 @pytest.mark.usefixtures("app", "autologin_user")
420 421 class TestRawFileHandling(object):
421 422
422 423 def test_download_file(self, backend):
423 424 commit = backend.repo.get_commit(commit_idx=173)
424 425 response = self.app.get(
425 426 route_path('repo_file_download',
426 427 repo_name=backend.repo_name,
427 428 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
428 429
429 430 assert response.content_disposition == "attachment; filename=nodes.py"
430 431 assert response.content_type == "text/x-python"
431 432
432 433 def test_download_file_wrong_cs(self, backend):
433 434 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
434 435
435 436 response = self.app.get(
436 437 route_path('repo_file_download',
437 438 repo_name=backend.repo_name,
438 439 commit_id=raw_id, f_path='vcs/nodes.svg'),
439 440 status=404)
440 441
441 442 msg = """No such commit exists for this repository"""
442 443 response.mustcontain(msg)
443 444
444 445 def test_download_file_wrong_f_path(self, backend):
445 446 commit = backend.repo.get_commit(commit_idx=173)
446 447 f_path = 'vcs/ERRORnodes.py'
447 448
448 449 response = self.app.get(
449 450 route_path('repo_file_download',
450 451 repo_name=backend.repo_name,
451 452 commit_id=commit.raw_id, f_path=f_path),
452 453 status=404)
453 454
454 455 msg = (
455 456 "There is no file nor directory at the given path: "
456 457 "`%s` at commit %s" % (f_path, commit.short_id))
457 458 response.mustcontain(msg)
458 459
459 460 def test_file_raw(self, backend):
460 461 commit = backend.repo.get_commit(commit_idx=173)
461 462 response = self.app.get(
462 463 route_path('repo_file_raw',
463 464 repo_name=backend.repo_name,
464 465 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
465 466
466 467 assert response.content_type == "text/plain"
467 468
468 469 def test_file_raw_binary(self, backend):
469 470 commit = backend.repo.get_commit()
470 471 response = self.app.get(
471 472 route_path('repo_file_raw',
472 473 repo_name=backend.repo_name,
473 474 commit_id=commit.raw_id,
474 475 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
475 476
476 477 assert response.content_disposition == 'inline'
477 478
478 479 def test_raw_file_wrong_cs(self, backend):
479 480 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
480 481
481 482 response = self.app.get(
482 483 route_path('repo_file_raw',
483 484 repo_name=backend.repo_name,
484 485 commit_id=raw_id, f_path='vcs/nodes.svg'),
485 486 status=404)
486 487
487 488 msg = """No such commit exists for this repository"""
488 489 response.mustcontain(msg)
489 490
490 491 def test_raw_wrong_f_path(self, backend):
491 492 commit = backend.repo.get_commit(commit_idx=173)
492 493 f_path = 'vcs/ERRORnodes.py'
493 494 response = self.app.get(
494 495 route_path('repo_file_raw',
495 496 repo_name=backend.repo_name,
496 497 commit_id=commit.raw_id, f_path=f_path),
497 498 status=404)
498 499
499 500 msg = (
500 501 "There is no file nor directory at the given path: "
501 502 "`%s` at commit %s" % (f_path, commit.short_id))
502 503 response.mustcontain(msg)
503 504
504 505 def test_raw_svg_should_not_be_rendered(self, backend):
505 506 backend.create_repo()
506 507 backend.ensure_file("xss.svg")
507 508 response = self.app.get(
508 509 route_path('repo_file_raw',
509 510 repo_name=backend.repo_name,
510 511 commit_id='tip', f_path='xss.svg'),)
511 512 # If the content type is image/svg+xml then it allows to render HTML
512 513 # and malicious SVG.
513 514 assert response.content_type == "text/plain"
514 515
515 516
516 517 @pytest.mark.usefixtures("app")
517 518 class TestRepositoryArchival(object):
518 519
519 520 def test_archival(self, backend):
520 521 backend.enable_downloads()
521 522 commit = backend.repo.get_commit(commit_idx=173)
522 523 for archive, info in settings.ARCHIVE_SPECS.items():
523 524 mime_type, arch_ext = info
524 525 short = commit.short_id + arch_ext
525 526 fname = commit.raw_id + arch_ext
526 527 filename = '%s-%s' % (backend.repo_name, short)
527 528 response = self.app.get(
528 529 route_path('repo_archivefile',
529 530 repo_name=backend.repo_name,
530 531 fname=fname))
531 532
532 533 assert response.status == '200 OK'
533 534 headers = [
534 535 ('Content-Disposition', 'attachment; filename=%s' % filename),
535 536 ('Content-Type', '%s' % mime_type),
536 537 ]
537 538
538 539 for header in headers:
539 540 assert header in response.headers.items()
540 541
541 542 @pytest.mark.parametrize('arch_ext',[
542 543 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
543 544 def test_archival_wrong_ext(self, backend, arch_ext):
544 545 backend.enable_downloads()
545 546 commit = backend.repo.get_commit(commit_idx=173)
546 547
547 548 fname = commit.raw_id + '.' + arch_ext
548 549
549 550 response = self.app.get(
550 551 route_path('repo_archivefile',
551 552 repo_name=backend.repo_name,
552 553 fname=fname))
553 554 response.mustcontain(
554 555 'Unknown archive type for: `{}`'.format(fname))
555 556
556 557 @pytest.mark.parametrize('commit_id', [
557 558 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
558 559 def test_archival_wrong_commit_id(self, backend, commit_id):
559 560 backend.enable_downloads()
560 561 fname = '%s.zip' % commit_id
561 562
562 563 response = self.app.get(
563 564 route_path('repo_archivefile',
564 565 repo_name=backend.repo_name,
565 566 fname=fname))
566 567 response.mustcontain('Unknown commit_id')
567 568
568 569
569 570 @pytest.mark.usefixtures("app")
570 571 class TestFilesDiff(object):
571 572
572 573 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
573 574 def test_file_full_diff(self, backend, diff):
574 575 commit1 = backend.repo.get_commit(commit_idx=-1)
575 576 commit2 = backend.repo.get_commit(commit_idx=-2)
576 577
577 578 response = self.app.get(
578 579 route_path('repo_files_diff',
579 580 repo_name=backend.repo_name,
580 581 f_path='README'),
581 582 params={
582 583 'diff1': commit2.raw_id,
583 584 'diff2': commit1.raw_id,
584 585 'fulldiff': '1',
585 586 'diff': diff,
586 587 })
587 588
588 589 if diff == 'diff':
589 590 # use redirect since this is OLD view redirecting to compare page
590 591 response = response.follow()
591 592
592 593 # It's a symlink to README.rst
593 594 response.mustcontain('README.rst')
594 595 response.mustcontain('No newline at end of file')
595 596
596 597 def test_file_binary_diff(self, backend):
597 598 commits = [
598 599 {'message': 'First commit'},
599 600 {'message': 'Commit with binary',
600 601 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
601 602 ]
602 603 repo = backend.create_repo(commits=commits)
603 604
604 605 response = self.app.get(
605 606 route_path('repo_files_diff',
606 607 repo_name=backend.repo_name,
607 608 f_path='file.bin'),
608 609 params={
609 610 'diff1': repo.get_commit(commit_idx=0).raw_id,
610 611 'diff2': repo.get_commit(commit_idx=1).raw_id,
611 612 'fulldiff': '1',
612 613 'diff': 'diff',
613 614 })
614 615 # use redirect since this is OLD view redirecting to compare page
615 616 response = response.follow()
616 617 response.mustcontain('Expand 1 commit')
617 618 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
618 619
619 620 if backend.alias == 'svn':
620 621 response.mustcontain('new file 10644')
621 622 # TODO(marcink): SVN doesn't yet detect binary changes
622 623 else:
623 624 response.mustcontain('new file 100644')
624 625 response.mustcontain('binary diff hidden')
625 626
626 627 def test_diff_2way(self, backend):
627 628 commit1 = backend.repo.get_commit(commit_idx=-1)
628 629 commit2 = backend.repo.get_commit(commit_idx=-2)
629 630 response = self.app.get(
630 631 route_path('repo_files_diff_2way_redirect',
631 632 repo_name=backend.repo_name,
632 633 f_path='README'),
633 634 params={
634 635 'diff1': commit2.raw_id,
635 636 'diff2': commit1.raw_id,
636 637 })
637 638 # use redirect since this is OLD view redirecting to compare page
638 639 response = response.follow()
639 640
640 641 # It's a symlink to README.rst
641 642 response.mustcontain('README.rst')
642 643 response.mustcontain('No newline at end of file')
643 644
644 645 def test_requires_one_commit_id(self, backend, autologin_user):
645 646 response = self.app.get(
646 647 route_path('repo_files_diff',
647 648 repo_name=backend.repo_name,
648 649 f_path='README.rst'),
649 650 status=400)
650 651 response.mustcontain(
651 652 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
652 653
653 654 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
654 655 repo = vcsbackend.repo
655 656 response = self.app.get(
656 657 route_path('repo_files_diff',
657 658 repo_name=repo.name,
658 659 f_path='does-not-exist-in-any-commit'),
659 660 params={
660 661 'diff1': repo[0].raw_id,
661 662 'diff2': repo[1].raw_id
662 663 })
663 664
664 665 response = response.follow()
665 666 response.mustcontain('No files')
666 667
667 668 def test_returns_redirect_if_file_not_changed(self, backend):
668 669 commit = backend.repo.get_commit(commit_idx=-1)
669 670 response = self.app.get(
670 671 route_path('repo_files_diff_2way_redirect',
671 672 repo_name=backend.repo_name,
672 673 f_path='README'),
673 674 params={
674 675 'diff1': commit.raw_id,
675 676 'diff2': commit.raw_id,
676 677 })
677 678
678 679 response = response.follow()
679 680 response.mustcontain('No files')
680 681 response.mustcontain('No commits in this compare')
681 682
682 683 def test_supports_diff_to_different_path_svn(self, backend_svn):
683 684 #TODO: check this case
684 685 return
685 686
686 687 repo = backend_svn['svn-simple-layout'].scm_instance()
687 688 commit_id_1 = '24'
688 689 commit_id_2 = '26'
689 690
690 691 response = self.app.get(
691 692 route_path('repo_files_diff',
692 693 repo_name=backend_svn.repo_name,
693 694 f_path='trunk/example.py'),
694 695 params={
695 696 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
696 697 'diff2': commit_id_2,
697 698 })
698 699
699 700 response = response.follow()
700 701 response.mustcontain(
701 702 # diff contains this
702 703 "Will print out a useful message on invocation.")
703 704
704 705 # Note: Expecting that we indicate the user what's being compared
705 706 response.mustcontain("trunk/example.py")
706 707 response.mustcontain("tags/v0.2/example.py")
707 708
708 709 def test_show_rev_redirects_to_svn_path(self, backend_svn):
709 710 #TODO: check this case
710 711 return
711 712
712 713 repo = backend_svn['svn-simple-layout'].scm_instance()
713 714 commit_id = repo[-1].raw_id
714 715
715 716 response = self.app.get(
716 717 route_path('repo_files_diff',
717 718 repo_name=backend_svn.repo_name,
718 719 f_path='trunk/example.py'),
719 720 params={
720 721 'diff1': 'branches/argparse/example.py@' + commit_id,
721 722 'diff2': commit_id,
722 723 },
723 724 status=302)
724 725 response = response.follow()
725 726 assert response.headers['Location'].endswith(
726 727 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
727 728
728 729 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
729 730 #TODO: check this case
730 731 return
731 732
732 733 repo = backend_svn['svn-simple-layout'].scm_instance()
733 734 commit_id = repo[-1].raw_id
734 735 response = self.app.get(
735 736 route_path('repo_files_diff',
736 737 repo_name=backend_svn.repo_name,
737 738 f_path='trunk/example.py'),
738 739 params={
739 740 'diff1': 'branches/argparse/example.py@' + commit_id,
740 741 'diff2': commit_id,
741 742 'show_rev': 'Show at Revision',
742 743 'annotate': 'true',
743 744 },
744 745 status=302)
745 746 response = response.follow()
746 747 assert response.headers['Location'].endswith(
747 748 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
748 749
749 750
750 751 @pytest.mark.usefixtures("app", "autologin_user")
751 752 class TestModifyFilesWithWebInterface(object):
752 753
753 754 def test_add_file_view(self, backend):
754 755 self.app.get(
755 756 route_path('repo_files_add_file',
756 757 repo_name=backend.repo_name,
757 758 commit_id='tip', f_path='/')
758 759 )
759 760
760 761 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
761 762 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
762 763 repo = backend.create_repo()
763 764 filename = 'init.py'
764 765 response = self.app.post(
765 766 route_path('repo_files_create_file',
766 767 repo_name=backend.repo_name,
767 768 commit_id='tip', f_path='/'),
768 769 params={
769 770 'content': "",
770 771 'filename': filename,
771 772 'location': "",
772 773 'csrf_token': csrf_token,
773 774 },
774 775 status=302)
775 776 assert_session_flash(response,
776 777 'Successfully committed new file `{}`'.format(
777 778 os.path.join(filename)))
778 779
779 780 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
780 781 response = self.app.post(
781 782 route_path('repo_files_create_file',
782 783 repo_name=backend.repo_name,
783 784 commit_id='tip', f_path='/'),
784 785 params={
785 786 'content': "foo",
786 787 'csrf_token': csrf_token,
787 788 },
788 789 status=302)
789 790
790 791 assert_session_flash(response, 'No filename')
791 792
792 793 def test_add_file_into_repo_errors_and_no_commits(
793 794 self, backend, csrf_token):
794 795 repo = backend.create_repo()
795 796 # Create a file with no filename, it will display an error but
796 797 # the repo has no commits yet
797 798 response = self.app.post(
798 799 route_path('repo_files_create_file',
799 800 repo_name=repo.repo_name,
800 801 commit_id='tip', f_path='/'),
801 802 params={
802 803 'content': "foo",
803 804 'csrf_token': csrf_token,
804 805 },
805 806 status=302)
806 807
807 808 assert_session_flash(response, 'No filename')
808 809
809 810 # Not allowed, redirect to the summary
810 811 redirected = response.follow()
811 812 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
812 813
813 814 # As there are no commits, displays the summary page with the error of
814 815 # creating a file with no filename
815 816
816 817 assert redirected.request.path == summary_url
817 818
818 819 @pytest.mark.parametrize("location, filename", [
819 820 ('/abs', 'foo'),
820 821 ('../rel', 'foo'),
821 822 ('file/../foo', 'foo'),
822 823 ])
823 824 def test_add_file_into_repo_bad_filenames(
824 825 self, location, filename, backend, csrf_token):
825 826 response = self.app.post(
826 827 route_path('repo_files_create_file',
827 828 repo_name=backend.repo_name,
828 829 commit_id='tip', f_path='/'),
829 830 params={
830 831 'content': "foo",
831 832 'filename': filename,
832 833 'location': location,
833 834 'csrf_token': csrf_token,
834 835 },
835 836 status=302)
836 837
837 838 assert_session_flash(
838 839 response,
839 840 'The location specified must be a relative path and must not '
840 841 'contain .. in the path')
841 842
842 843 @pytest.mark.parametrize("cnt, location, filename", [
843 844 (1, '', 'foo.txt'),
844 845 (2, 'dir', 'foo.rst'),
845 846 (3, 'rel/dir', 'foo.bar'),
846 847 ])
847 848 def test_add_file_into_repo(self, cnt, location, filename, backend,
848 849 csrf_token):
849 850 repo = backend.create_repo()
850 851 response = self.app.post(
851 852 route_path('repo_files_create_file',
852 853 repo_name=repo.repo_name,
853 854 commit_id='tip', f_path='/'),
854 855 params={
855 856 'content': "foo",
856 857 'filename': filename,
857 858 'location': location,
858 859 'csrf_token': csrf_token,
859 860 },
860 861 status=302)
861 862 assert_session_flash(response,
862 863 'Successfully committed new file `{}`'.format(
863 864 os.path.join(location, filename)))
864 865
865 866 def test_edit_file_view(self, backend):
866 867 response = self.app.get(
867 868 route_path('repo_files_edit_file',
868 869 repo_name=backend.repo_name,
869 870 commit_id=backend.default_head_id,
870 871 f_path='vcs/nodes.py'),
871 872 status=200)
872 873 response.mustcontain("Module holding everything related to vcs nodes.")
873 874
874 875 def test_edit_file_view_not_on_branch(self, backend):
875 876 repo = backend.create_repo()
876 877 backend.ensure_file("vcs/nodes.py")
877 878
878 879 response = self.app.get(
879 880 route_path('repo_files_edit_file',
880 881 repo_name=repo.repo_name,
881 882 commit_id='tip',
882 883 f_path='vcs/nodes.py'),
883 884 status=302)
884 885 assert_session_flash(
885 886 response,
886 887 'You can only edit files with commit being a valid branch')
887 888
888 889 def test_edit_file_view_commit_changes(self, backend, csrf_token):
889 890 repo = backend.create_repo()
890 891 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
891 892
892 893 response = self.app.post(
893 894 route_path('repo_files_update_file',
894 895 repo_name=repo.repo_name,
895 896 commit_id=backend.default_head_id,
896 897 f_path='vcs/nodes.py'),
897 898 params={
898 899 'content': "print 'hello world'",
899 900 'message': 'I committed',
900 901 'filename': "vcs/nodes.py",
901 902 'csrf_token': csrf_token,
902 903 },
903 904 status=302)
904 905 assert_session_flash(
905 906 response, 'Successfully committed changes to file `vcs/nodes.py`')
906 907 tip = repo.get_commit(commit_idx=-1)
907 908 assert tip.message == 'I committed'
908 909
909 910 def test_edit_file_view_commit_changes_default_message(self, backend,
910 911 csrf_token):
911 912 repo = backend.create_repo()
912 913 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
913 914
914 915 commit_id = (
915 916 backend.default_branch_name or
916 917 backend.repo.scm_instance().commit_ids[-1])
917 918
918 919 response = self.app.post(
919 920 route_path('repo_files_update_file',
920 921 repo_name=repo.repo_name,
921 922 commit_id=commit_id,
922 923 f_path='vcs/nodes.py'),
923 924 params={
924 925 'content': "print 'hello world'",
925 926 'message': '',
926 927 'filename': "vcs/nodes.py",
927 928 'csrf_token': csrf_token,
928 929 },
929 930 status=302)
930 931 assert_session_flash(
931 932 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 933 tip = repo.get_commit(commit_idx=-1)
933 934 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
934 935
935 936 def test_delete_file_view(self, backend):
936 937 self.app.get(
937 938 route_path('repo_files_remove_file',
938 939 repo_name=backend.repo_name,
939 940 commit_id=backend.default_head_id,
940 941 f_path='vcs/nodes.py'),
941 942 status=200)
942 943
943 944 def test_delete_file_view_not_on_branch(self, backend):
944 945 repo = backend.create_repo()
945 946 backend.ensure_file('vcs/nodes.py')
946 947
947 948 response = self.app.get(
948 949 route_path('repo_files_remove_file',
949 950 repo_name=repo.repo_name,
950 951 commit_id='tip',
951 952 f_path='vcs/nodes.py'),
952 953 status=302)
953 954 assert_session_flash(
954 955 response,
955 956 'You can only delete files with commit being a valid branch')
956 957
957 958 def test_delete_file_view_commit_changes(self, backend, csrf_token):
958 959 repo = backend.create_repo()
959 960 backend.ensure_file("vcs/nodes.py")
960 961
961 962 response = self.app.post(
962 963 route_path('repo_files_delete_file',
963 964 repo_name=repo.repo_name,
964 965 commit_id=backend.default_head_id,
965 966 f_path='vcs/nodes.py'),
966 967 params={
967 968 'message': 'i commited',
968 969 'csrf_token': csrf_token,
969 970 },
970 971 status=302)
971 972 assert_session_flash(
972 973 response, 'Successfully deleted file `vcs/nodes.py`')
973 974
974 975
975 976 @pytest.mark.usefixtures("app")
976 977 class TestFilesViewOtherCases(object):
977 978
978 979 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
979 980 self, backend_stub, autologin_regular_user, user_regular,
980 981 user_util):
981 982
982 983 repo = backend_stub.create_repo()
983 984 user_util.grant_user_permission_to_repo(
984 985 repo, user_regular, 'repository.write')
985 986 response = self.app.get(
986 987 route_path('repo_files',
987 988 repo_name=repo.repo_name,
988 989 commit_id='tip', f_path='/'))
989 990
990 991 repo_file_add_url = route_path(
991 992 'repo_files_add_file',
992 993 repo_name=repo.repo_name,
993 994 commit_id=0, f_path='') + '#edit'
994 995
995 996 assert_session_flash(
996 997 response,
997 998 'There are no files yet. <a class="alert-link" '
998 999 'href="{}">Click here to add a new file.</a>'
999 1000 .format(repo_file_add_url))
1000 1001
1001 1002 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1002 self, backend_stub, user_util):
1003 self, backend_stub, autologin_regular_user):
1003 1004 repo = backend_stub.create_repo()
1005 # init session for anon user
1006 route_path('repo_summary', repo_name=repo.repo_name)
1007
1004 1008 repo_file_add_url = route_path(
1005 1009 'repo_files_add_file',
1006 1010 repo_name=repo.repo_name,
1007 1011 commit_id=0, f_path='') + '#edit'
1008 1012
1009 1013 response = self.app.get(
1010 1014 route_path('repo_files',
1011 1015 repo_name=repo.repo_name,
1012 1016 commit_id='tip', f_path='/'))
1013 1017
1014 1018 assert_session_flash(response, no_=repo_file_add_url)
1015 1019
1016 1020 @pytest.mark.parametrize('file_node', [
1017 1021 'archive/file.zip',
1018 1022 'diff/my-file.txt',
1019 1023 'render.py',
1020 1024 'render',
1021 1025 'remove_file',
1022 1026 'remove_file/to-delete.txt',
1023 1027 ])
1024 1028 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1025 1029 backend.create_repo()
1026 1030 backend.ensure_file(file_node)
1027 1031
1028 1032 self.app.get(
1029 1033 route_path('repo_files',
1030 1034 repo_name=backend.repo_name,
1031 1035 commit_id='tip', f_path=file_node),
1032 1036 status=200)
1033 1037
1034 1038
1035 1039 class TestAdjustFilePathForSvn(object):
1036 1040 """
1037 1041 SVN specific adjustments of node history in RepoFilesView.
1038 1042 """
1039 1043
1040 1044 def test_returns_path_relative_to_matched_reference(self):
1041 1045 repo = self._repo(branches=['trunk'])
1042 1046 self.assert_file_adjustment('trunk/file', 'file', repo)
1043 1047
1044 1048 def test_does_not_modify_file_if_no_reference_matches(self):
1045 1049 repo = self._repo(branches=['trunk'])
1046 1050 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1047 1051
1048 1052 def test_does_not_adjust_partial_directory_names(self):
1049 1053 repo = self._repo(branches=['trun'])
1050 1054 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1051 1055
1052 1056 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1053 1057 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1054 1058 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1055 1059
1056 1060 def assert_file_adjustment(self, f_path, expected, repo):
1057 1061 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1058 1062 assert result == expected
1059 1063
1060 1064 def _repo(self, branches=None):
1061 1065 repo = mock.Mock()
1062 1066 repo.branches = OrderedDict((name, '0') for name in branches or [])
1063 1067 repo.tags = {}
1064 1068 return repo
@@ -1,258 +1,259 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 datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
34 34 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
35 35 import rhodecode.lib.helpers as h
36 36 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.forms import RepoForkForm
39 39 from rhodecode.model.scm import ScmModel, RepoGroupList
40 40 from rhodecode.lib.utils2 import safe_int, safe_unicode
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoForksView(RepoAppView, DataGridAppView):
46 46
47 47 def load_default_context(self):
48 48 c = self._get_local_tmpl_context(include_app_defaults=True)
49 49 c.rhodecode_repo = self.rhodecode_vcs_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: safe_unicode(k[0]), c.repo_groups)
56 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
56 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
57 self.request.translate)
57 58 c.landing_revs_choices = choices
58 59 c.personal_repo_group = c.rhodecode_user.personal_repo_group
59 60
60
61 61 return c
62 62
63 63 @LoginRequired()
64 64 @HasRepoPermissionAnyDecorator(
65 65 'repository.read', 'repository.write', 'repository.admin')
66 66 @view_config(
67 67 route_name='repo_forks_show_all', request_method='GET',
68 68 renderer='rhodecode:templates/forks/forks.mako')
69 69 def repo_forks_show_all(self):
70 70 c = self.load_default_context()
71 71 return self._get_template_context(c)
72 72
73 73 @LoginRequired()
74 74 @HasRepoPermissionAnyDecorator(
75 75 'repository.read', 'repository.write', 'repository.admin')
76 76 @view_config(
77 77 route_name='repo_forks_data', request_method='GET',
78 78 renderer='json_ext', xhr=True)
79 79 def repo_forks_data(self):
80 80 _ = self.request.translate
81 self.load_default_context()
81 82 column_map = {
82 83 'fork_name': 'repo_name',
83 84 'fork_date': 'created_on',
84 85 'last_activity': 'updated_on'
85 86 }
86 87 draw, start, limit = self._extract_chunk(self.request)
87 88 search_q, order_by, order_dir = self._extract_ordering(
88 89 self.request, column_map=column_map)
89 90
90 91 acl_check = HasRepoPermissionAny(
91 92 'repository.read', 'repository.write', 'repository.admin')
92 93 repo_id = self.db_repo.repo_id
93 94 allowed_ids = [-1]
94 95 for f in Repository.query().filter(Repository.fork_id == repo_id):
95 96 if acl_check(f.repo_name, 'get forks check'):
96 97 allowed_ids.append(f.repo_id)
97 98
98 99 forks_data_total_count = Repository.query()\
99 100 .filter(Repository.fork_id == repo_id)\
100 101 .filter(Repository.repo_id.in_(allowed_ids))\
101 102 .count()
102 103
103 104 # json generate
104 105 base_q = Repository.query()\
105 106 .filter(Repository.fork_id == repo_id)\
106 107 .filter(Repository.repo_id.in_(allowed_ids))\
107 108
108 109 if search_q:
109 110 like_expression = u'%{}%'.format(safe_unicode(search_q))
110 111 base_q = base_q.filter(or_(
111 112 Repository.repo_name.ilike(like_expression),
112 113 Repository.description.ilike(like_expression),
113 114 ))
114 115
115 116 forks_data_total_filtered_count = base_q.count()
116 117
117 118 sort_col = getattr(Repository, order_by, None)
118 119 if sort_col:
119 120 if order_dir == 'asc':
120 121 # handle null values properly to order by NULL last
121 122 if order_by in ['last_activity']:
122 123 sort_col = coalesce(sort_col, datetime.date.max)
123 124 sort_col = sort_col.asc()
124 125 else:
125 126 # handle null values properly to order by NULL last
126 127 if order_by in ['last_activity']:
127 128 sort_col = coalesce(sort_col, datetime.date.min)
128 129 sort_col = sort_col.desc()
129 130
130 131 base_q = base_q.order_by(sort_col)
131 132 base_q = base_q.offset(start).limit(limit)
132 133
133 134 fork_list = base_q.all()
134 135
135 136 def fork_actions(fork):
136 137 url_link = h.route_path(
137 138 'repo_compare',
138 139 repo_name=fork.repo_name,
139 140 source_ref_type=self.db_repo.landing_rev[0],
140 141 source_ref=self.db_repo.landing_rev[1],
141 142 target_ref_type=self.db_repo.landing_rev[0],
142 143 target_ref=self.db_repo.landing_rev[1],
143 144 _query=dict(merge=1, target_repo=f.repo_name))
144 145 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
145 146
146 147 def fork_name(fork):
147 148 return h.link_to(fork.repo_name,
148 149 h.route_path('repo_summary', repo_name=fork.repo_name))
149 150
150 151 forks_data = []
151 152 for fork in fork_list:
152 153 forks_data.append({
153 154 "username": h.gravatar_with_user(self.request, fork.user.username),
154 155 "fork_name": fork_name(fork),
155 156 "description": fork.description,
156 157 "fork_date": h.age_component(fork.created_on, time_is_local=True),
157 158 "last_activity": h.format_date(fork.updated_on),
158 159 "action": fork_actions(fork),
159 160 })
160 161
161 162 data = ({
162 163 'draw': draw,
163 164 'data': forks_data,
164 165 'recordsTotal': forks_data_total_count,
165 166 'recordsFiltered': forks_data_total_filtered_count,
166 167 })
167 168
168 169 return data
169 170
170 171 @LoginRequired()
171 172 @NotAnonymous()
172 173 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
173 174 @HasRepoPermissionAnyDecorator(
174 175 'repository.read', 'repository.write', 'repository.admin')
175 176 @view_config(
176 177 route_name='repo_fork_new', request_method='GET',
177 178 renderer='rhodecode:templates/forks/forks.mako')
178 179 def repo_fork_new(self):
179 180 c = self.load_default_context()
180 181
181 182 defaults = RepoModel()._get_defaults(self.db_repo_name)
182 183 # alter the description to indicate a fork
183 184 defaults['description'] = (
184 185 'fork of repository: %s \n%s' % (
185 186 defaults['repo_name'], defaults['description']))
186 187 # add suffix to fork
187 188 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
188 189
189 190 data = render('rhodecode:templates/forks/fork.mako',
190 191 self._get_template_context(c), self.request)
191 192 html = formencode.htmlfill.render(
192 193 data,
193 194 defaults=defaults,
194 195 encoding="UTF-8",
195 196 force_defaults=False
196 197 )
197 198 return Response(html)
198 199
199 200 @LoginRequired()
200 201 @NotAnonymous()
201 202 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
202 203 @HasRepoPermissionAnyDecorator(
203 204 'repository.read', 'repository.write', 'repository.admin')
204 205 @CSRFRequired()
205 206 @view_config(
206 207 route_name='repo_fork_create', request_method='POST',
207 208 renderer='rhodecode:templates/forks/fork.mako')
208 209 def repo_fork_create(self):
209 210 _ = self.request.translate
210 211 c = self.load_default_context()
211 212
212 213 _form = RepoForkForm(self.request.translate, old_data={'repo_type': self.db_repo.repo_type},
213 214 repo_groups=c.repo_groups_choices,
214 215 landing_revs=c.landing_revs_choices)()
215 216 post_data = dict(self.request.POST)
216 217
217 218 # forbid injecting other repo by forging a request
218 219 post_data['fork_parent_id'] = self.db_repo.repo_id
219 220
220 221 form_result = {}
221 222 task_id = None
222 223 try:
223 224 form_result = _form.to_python(post_data)
224 225 # create fork is done sometimes async on celery, db transaction
225 226 # management is handled there.
226 227 task = RepoModel().create_fork(
227 228 form_result, c.rhodecode_user.user_id)
228 229 from celery.result import BaseAsyncResult
229 230 if isinstance(task, BaseAsyncResult):
230 231 task_id = task.task_id
231 232 except formencode.Invalid as errors:
232 233 c.rhodecode_db_repo = self.db_repo
233 234
234 235 data = render('rhodecode:templates/forks/fork.mako',
235 236 self._get_template_context(c), self.request)
236 237 html = formencode.htmlfill.render(
237 238 data,
238 239 defaults=errors.value,
239 240 errors=errors.error_dict or {},
240 241 prefix_error=False,
241 242 encoding="UTF-8",
242 243 force_defaults=False
243 244 )
244 245 return Response(html)
245 246 except Exception:
246 247 log.exception(
247 248 u'Exception while trying to fork the repository %s',
248 249 self.db_repo_name)
249 250 msg = (
250 251 _('An error occurred during repository forking %s') % (
251 252 self.db_repo_name, ))
252 253 h.flash(msg, category='error')
253 254
254 255 repo_name = form_result.get('repo_name_full', self.db_repo_name)
255 256 raise HTTPFound(
256 257 h.route_path('repo_creating',
257 258 repo_name=repo_name,
258 259 _query=dict(task_id=task_id)))
@@ -1,1238 +1,1244 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 collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'rhodecode:templates/data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 self.load_default_context()
176 177
177 178 # additional filters
178 179 req_get = self.request.GET
179 180 source = str2bool(req_get.get('source'))
180 181 closed = str2bool(req_get.get('closed'))
181 182 my = str2bool(req_get.get('my'))
182 183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 185
185 186 filter_type = 'awaiting_review' if awaiting_review \
186 187 else 'awaiting_my_review' if awaiting_my_review \
187 188 else None
188 189
189 190 opened_by = None
190 191 if my:
191 192 opened_by = [self._rhodecode_user.user_id]
192 193
193 194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 195 if closed:
195 196 statuses = [PullRequest.STATUS_CLOSED]
196 197
197 198 data = self._get_pull_requests_list(
198 199 repo_name=self.db_repo_name, source=source,
199 200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 201
201 202 return data
202 203
203 204 def _get_pr_version(self, pull_request_id, version=None):
204 205 at_version = None
205 206
206 207 if version and version == 'latest':
207 208 pull_request_ver = PullRequest.get(pull_request_id)
208 209 pull_request_obj = pull_request_ver
209 210 _org_pull_request_obj = pull_request_obj
210 211 at_version = 'latest'
211 212 elif version:
212 213 pull_request_ver = PullRequestVersion.get_or_404(version)
213 214 pull_request_obj = pull_request_ver
214 215 _org_pull_request_obj = pull_request_ver.pull_request
215 216 at_version = pull_request_ver.pull_request_version_id
216 217 else:
217 218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 219 pull_request_id)
219 220
220 221 pull_request_display_obj = PullRequest.get_pr_display_object(
221 222 pull_request_obj, _org_pull_request_obj)
222 223
223 224 return _org_pull_request_obj, pull_request_obj, \
224 225 pull_request_display_obj, at_version
225 226
226 227 def _get_diffset(self, source_repo_name, source_repo,
227 228 source_ref_id, target_ref_id,
228 229 target_commit, source_commit, diff_limit, fulldiff,
229 230 file_limit, display_inline_comments):
230 231
231 232 vcs_diff = PullRequestModel().get_diff(
232 233 source_repo, source_ref_id, target_ref_id)
233 234
234 235 diff_processor = diffs.DiffProcessor(
235 236 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 237 file_limit=file_limit, show_full_diff=fulldiff)
237 238
238 239 _parsed = diff_processor.prepare()
239 240
240 241 def _node_getter(commit):
241 242 def get_node(fname):
242 243 try:
243 244 return commit.get_node(fname)
244 245 except NodeDoesNotExistError:
245 246 return None
246 247
247 248 return get_node
248 249
249 250 diffset = codeblocks.DiffSet(
250 251 repo_name=self.db_repo_name,
251 252 source_repo_name=source_repo_name,
252 253 source_node_getter=_node_getter(target_commit),
253 254 target_node_getter=_node_getter(source_commit),
254 255 comments=display_inline_comments
255 256 )
256 257 diffset = diffset.render_patchset(
257 258 _parsed, target_commit.raw_id, source_commit.raw_id)
258 259
259 260 return diffset
260 261
261 262 @LoginRequired()
262 263 @HasRepoPermissionAnyDecorator(
263 264 'repository.read', 'repository.write', 'repository.admin')
264 265 @view_config(
265 266 route_name='pullrequest_show', request_method='GET',
266 267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 268 def pull_request_show(self):
268 269 pull_request_id = self.request.matchdict['pull_request_id']
269 270
270 271 c = self.load_default_context()
271 272
272 273 version = self.request.GET.get('version')
273 274 from_version = self.request.GET.get('from_version') or version
274 275 merge_checks = self.request.GET.get('merge_checks')
275 276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 277
277 278 (pull_request_latest,
278 279 pull_request_at_ver,
279 280 pull_request_display_obj,
280 281 at_version) = self._get_pr_version(
281 282 pull_request_id, version=version)
282 283 pr_closed = pull_request_latest.is_closed()
283 284
284 285 if pr_closed and (version or from_version):
285 286 # not allow to browse versions
286 287 raise HTTPFound(h.route_path(
287 288 'pullrequest_show', repo_name=self.db_repo_name,
288 289 pull_request_id=pull_request_id))
289 290
290 291 versions = pull_request_display_obj.versions()
291 292
292 293 c.at_version = at_version
293 294 c.at_version_num = (at_version
294 295 if at_version and at_version != 'latest'
295 296 else None)
296 297 c.at_version_pos = ChangesetComment.get_index_from_version(
297 298 c.at_version_num, versions)
298 299
299 300 (prev_pull_request_latest,
300 301 prev_pull_request_at_ver,
301 302 prev_pull_request_display_obj,
302 303 prev_at_version) = self._get_pr_version(
303 304 pull_request_id, version=from_version)
304 305
305 306 c.from_version = prev_at_version
306 307 c.from_version_num = (prev_at_version
307 308 if prev_at_version and prev_at_version != 'latest'
308 309 else None)
309 310 c.from_version_pos = ChangesetComment.get_index_from_version(
310 311 c.from_version_num, versions)
311 312
312 313 # define if we're in COMPARE mode or VIEW at version mode
313 314 compare = at_version != prev_at_version
314 315
315 316 # pull_requests repo_name we opened it against
316 317 # ie. target_repo must match
317 318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 319 raise HTTPNotFound()
319 320
320 321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 322 pull_request_at_ver)
322 323
323 324 c.pull_request = pull_request_display_obj
324 325 c.pull_request_latest = pull_request_latest
325 326
326 327 if compare or (at_version and not at_version == 'latest'):
327 328 c.allowed_to_change_status = False
328 329 c.allowed_to_update = False
329 330 c.allowed_to_merge = False
330 331 c.allowed_to_delete = False
331 332 c.allowed_to_comment = False
332 333 c.allowed_to_close = False
333 334 else:
334 335 can_change_status = PullRequestModel().check_user_change_status(
335 336 pull_request_at_ver, self._rhodecode_user)
336 337 c.allowed_to_change_status = can_change_status and not pr_closed
337 338
338 339 c.allowed_to_update = PullRequestModel().check_user_update(
339 340 pull_request_latest, self._rhodecode_user) and not pr_closed
340 341 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 342 pull_request_latest, self._rhodecode_user) and not pr_closed
342 343 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 344 pull_request_latest, self._rhodecode_user) and not pr_closed
344 345 c.allowed_to_comment = not pr_closed
345 346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 347
347 348 c.forbid_adding_reviewers = False
348 349 c.forbid_author_to_review = False
349 350 c.forbid_commit_author_to_review = False
350 351
351 352 if pull_request_latest.reviewer_data and \
352 353 'rules' in pull_request_latest.reviewer_data:
353 354 rules = pull_request_latest.reviewer_data['rules'] or {}
354 355 try:
355 356 c.forbid_adding_reviewers = rules.get(
356 357 'forbid_adding_reviewers')
357 358 c.forbid_author_to_review = rules.get(
358 359 'forbid_author_to_review')
359 360 c.forbid_commit_author_to_review = rules.get(
360 361 'forbid_commit_author_to_review')
361 362 except Exception:
362 363 pass
363 364
364 365 # check merge capabilities
365 366 _merge_check = MergeCheck.validate(
366 367 pull_request_latest, user=self._rhodecode_user,
367 368 translator=self.request.translate)
368 369 c.pr_merge_errors = _merge_check.error_details
369 370 c.pr_merge_possible = not _merge_check.failed
370 371 c.pr_merge_message = _merge_check.merge_msg
371 372
372 373 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 374 pull_request_latest, translator=self.request.translate)
374 375
375 376 c.pull_request_review_status = _merge_check.review_status
376 377 if merge_checks:
377 378 self.request.override_renderer = \
378 379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 380 return self._get_template_context(c)
380 381
381 382 comments_model = CommentsModel()
382 383
383 384 # reviewers and statuses
384 385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386 387
387 388 # GENERAL COMMENTS with versions #
388 389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 390 q = q.order_by(ChangesetComment.comment_id.asc())
390 391 general_comments = q
391 392
392 393 # pick comments we want to render at current version
393 394 c.comment_versions = comments_model.aggregate_comments(
394 395 general_comments, versions, c.at_version_num)
395 396 c.comments = c.comment_versions[c.at_version_num]['until']
396 397
397 398 # INLINE COMMENTS with versions #
398 399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 400 q = q.order_by(ChangesetComment.comment_id.asc())
400 401 inline_comments = q
401 402
402 403 c.inline_versions = comments_model.aggregate_comments(
403 404 inline_comments, versions, c.at_version_num, inline=True)
404 405
405 406 # inject latest version
406 407 latest_ver = PullRequest.get_pr_display_object(
407 408 pull_request_latest, pull_request_latest)
408 409
409 410 c.versions = versions + [latest_ver]
410 411
411 412 # if we use version, then do not show later comments
412 413 # than current version
413 414 display_inline_comments = collections.defaultdict(
414 415 lambda: collections.defaultdict(list))
415 416 for co in inline_comments:
416 417 if c.at_version_num:
417 418 # pick comments that are at least UPTO given version, so we
418 419 # don't render comments for higher version
419 420 should_render = co.pull_request_version_id and \
420 421 co.pull_request_version_id <= c.at_version_num
421 422 else:
422 423 # showing all, for 'latest'
423 424 should_render = True
424 425
425 426 if should_render:
426 427 display_inline_comments[co.f_path][co.line_no].append(co)
427 428
428 429 # load diff data into template context, if we use compare mode then
429 430 # diff is calculated based on changes between versions of PR
430 431
431 432 source_repo = pull_request_at_ver.source_repo
432 433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433 434
434 435 target_repo = pull_request_at_ver.target_repo
435 436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436 437
437 438 if compare:
438 439 # in compare switch the diff base to latest commit from prev version
439 440 target_ref_id = prev_pull_request_display_obj.revisions[0]
440 441
441 442 # despite opening commits for bookmarks/branches/tags, we always
442 443 # convert this to rev to prevent changes after bookmark or branch change
443 444 c.source_ref_type = 'rev'
444 445 c.source_ref = source_ref_id
445 446
446 447 c.target_ref_type = 'rev'
447 448 c.target_ref = target_ref_id
448 449
449 450 c.source_repo = source_repo
450 451 c.target_repo = target_repo
451 452
452 453 c.commit_ranges = []
453 454 source_commit = EmptyCommit()
454 455 target_commit = EmptyCommit()
455 456 c.missing_requirements = False
456 457
457 458 source_scm = source_repo.scm_instance()
458 459 target_scm = target_repo.scm_instance()
459 460
460 461 # try first shadow repo, fallback to regular repo
461 462 try:
462 463 commits_source_repo = pull_request_latest.get_shadow_repo()
463 464 except Exception:
464 465 log.debug('Failed to get shadow repo', exc_info=True)
465 466 commits_source_repo = source_scm
466 467
467 468 c.commits_source_repo = commits_source_repo
468 469 commit_cache = {}
469 470 try:
470 471 pre_load = ["author", "branch", "date", "message"]
471 472 show_revs = pull_request_at_ver.revisions
472 473 for rev in show_revs:
473 474 comm = commits_source_repo.get_commit(
474 475 commit_id=rev, pre_load=pre_load)
475 476 c.commit_ranges.append(comm)
476 477 commit_cache[comm.raw_id] = comm
477 478
478 479 # Order here matters, we first need to get target, and then
479 480 # the source
480 481 target_commit = commits_source_repo.get_commit(
481 482 commit_id=safe_str(target_ref_id))
482 483
483 484 source_commit = commits_source_repo.get_commit(
484 485 commit_id=safe_str(source_ref_id))
485 486
486 487 except CommitDoesNotExistError:
487 488 log.warning(
488 489 'Failed to get commit from `{}` repo'.format(
489 490 commits_source_repo), exc_info=True)
490 491 except RepositoryRequirementError:
491 492 log.warning(
492 493 'Failed to get all required data from repo', exc_info=True)
493 494 c.missing_requirements = True
494 495
495 496 c.ancestor = None # set it to None, to hide it from PR view
496 497
497 498 try:
498 499 ancestor_id = source_scm.get_common_ancestor(
499 500 source_commit.raw_id, target_commit.raw_id, target_scm)
500 501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 502 except Exception:
502 503 c.ancestor_commit = None
503 504
504 505 c.statuses = source_repo.statuses(
505 506 [x.raw_id for x in c.commit_ranges])
506 507
507 508 # auto collapse if we have more than limit
508 509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 511 c.compare_mode = compare
511 512
512 513 # diff_limit is the old behavior, will cut off the whole diff
513 514 # if the limit is applied otherwise will just hide the
514 515 # big files from the front-end
515 516 diff_limit = c.visual.cut_off_limit_diff
516 517 file_limit = c.visual.cut_off_limit_file
517 518
518 519 c.missing_commits = False
519 520 if (c.missing_requirements
520 521 or isinstance(source_commit, EmptyCommit)
521 522 or source_commit == target_commit):
522 523
523 524 c.missing_commits = True
524 525 else:
525 526
526 527 c.diffset = self._get_diffset(
527 528 c.source_repo.repo_name, commits_source_repo,
528 529 source_ref_id, target_ref_id,
529 530 target_commit, source_commit,
530 531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531 532
532 533 c.limited_diff = c.diffset.limited_diff
533 534
534 535 # calculate removed files that are bound to comments
535 536 comment_deleted_files = [
536 537 fname for fname in display_inline_comments
537 538 if fname not in c.diffset.file_stats]
538 539
539 540 c.deleted_files_comments = collections.defaultdict(dict)
540 541 for fname, per_line_comments in display_inline_comments.items():
541 542 if fname in comment_deleted_files:
542 543 c.deleted_files_comments[fname]['stats'] = 0
543 544 c.deleted_files_comments[fname]['comments'] = list()
544 545 for lno, comments in per_line_comments.items():
545 546 c.deleted_files_comments[fname]['comments'].extend(
546 547 comments)
547 548
548 549 # this is a hack to properly display links, when creating PR, the
549 550 # compare view and others uses different notation, and
550 551 # compare_commits.mako renders links based on the target_repo.
551 552 # We need to swap that here to generate it properly on the html side
552 553 c.target_repo = c.source_repo
553 554
554 555 c.commit_statuses = ChangesetStatus.STATUSES
555 556
556 557 c.show_version_changes = not pr_closed
557 558 if c.show_version_changes:
558 559 cur_obj = pull_request_at_ver
559 560 prev_obj = prev_pull_request_at_ver
560 561
561 562 old_commit_ids = prev_obj.revisions
562 563 new_commit_ids = cur_obj.revisions
563 564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 565 old_commit_ids, new_commit_ids)
565 566 c.commit_changes_summary = commit_changes
566 567
567 568 # calculate the diff for commits between versions
568 569 c.commit_changes = []
569 570 mark = lambda cs, fw: list(
570 571 h.itertools.izip_longest([], cs, fillvalue=fw))
571 572 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 573 + mark(commit_changes.removed, 'r') \
573 574 + mark(commit_changes.common, 'c'):
574 575
575 576 if raw_id in commit_cache:
576 577 commit = commit_cache[raw_id]
577 578 else:
578 579 try:
579 580 commit = commits_source_repo.get_commit(raw_id)
580 581 except CommitDoesNotExistError:
581 582 # in case we fail extracting still use "dummy" commit
582 583 # for display in commit diff
583 584 commit = h.AttributeDict(
584 585 {'raw_id': raw_id,
585 586 'message': 'EMPTY or MISSING COMMIT'})
586 587 c.commit_changes.append([c_type, commit])
587 588
588 589 # current user review statuses for each version
589 590 c.review_versions = {}
590 591 if self._rhodecode_user.user_id in allowed_reviewers:
591 592 for co in general_comments:
592 593 if co.author.user_id == self._rhodecode_user.user_id:
593 594 # each comment has a status change
594 595 status = co.status_change
595 596 if status:
596 597 _ver_pr = status[0].comment.pull_request_version_id
597 598 c.review_versions[_ver_pr] = status[0]
598 599
599 600 return self._get_template_context(c)
600 601
601 602 def assure_not_empty_repo(self):
602 603 _ = self.request.translate
603 604
604 605 try:
605 606 self.db_repo.scm_instance().get_commit()
606 607 except EmptyRepositoryError:
607 608 h.flash(h.literal(_('There are no commits yet')),
608 609 category='warning')
609 610 raise HTTPFound(
610 611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611 612
612 613 @LoginRequired()
613 614 @NotAnonymous()
614 615 @HasRepoPermissionAnyDecorator(
615 616 'repository.read', 'repository.write', 'repository.admin')
616 617 @view_config(
617 618 route_name='pullrequest_new', request_method='GET',
618 619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 620 def pull_request_new(self):
620 621 _ = self.request.translate
621 622 c = self.load_default_context()
622 623
623 624 self.assure_not_empty_repo()
624 625 source_repo = self.db_repo
625 626
626 627 commit_id = self.request.GET.get('commit')
627 628 branch_ref = self.request.GET.get('branch')
628 629 bookmark_ref = self.request.GET.get('bookmark')
629 630
630 631 try:
631 632 source_repo_data = PullRequestModel().generate_repo_data(
632 633 source_repo, commit_id=commit_id,
633 634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 635 except CommitDoesNotExistError as e:
635 636 log.exception(e)
636 637 h.flash(_('Commit does not exist'), 'error')
637 638 raise HTTPFound(
638 639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639 640
640 641 default_target_repo = source_repo
641 642
642 643 if source_repo.parent:
643 644 parent_vcs_obj = source_repo.parent.scm_instance()
644 645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 646 # change default if we have a parent repo
646 647 default_target_repo = source_repo.parent
647 648
648 649 target_repo_data = PullRequestModel().generate_repo_data(
649 650 default_target_repo, translator=self.request.translate)
650 651
651 652 selected_source_ref = source_repo_data['refs']['selected_ref']
652 653
653 654 title_source_ref = selected_source_ref.split(':', 2)[1]
654 655 c.default_title = PullRequestModel().generate_pullrequest_title(
655 656 source=source_repo.repo_name,
656 657 source_ref=title_source_ref,
657 658 target=default_target_repo.repo_name
658 659 )
659 660
660 661 c.default_repo_data = {
661 662 'source_repo_name': source_repo.repo_name,
662 663 'source_refs_json': json.dumps(source_repo_data),
663 664 'target_repo_name': default_target_repo.repo_name,
664 665 'target_refs_json': json.dumps(target_repo_data),
665 666 }
666 667 c.default_source_ref = selected_source_ref
667 668
668 669 return self._get_template_context(c)
669 670
670 671 @LoginRequired()
671 672 @NotAnonymous()
672 673 @HasRepoPermissionAnyDecorator(
673 674 'repository.read', 'repository.write', 'repository.admin')
674 675 @view_config(
675 676 route_name='pullrequest_repo_refs', request_method='GET',
676 677 renderer='json_ext', xhr=True)
677 678 def pull_request_repo_refs(self):
679 self.load_default_context()
678 680 target_repo_name = self.request.matchdict['target_repo_name']
679 681 repo = Repository.get_by_repo_name(target_repo_name)
680 682 if not repo:
681 683 raise HTTPNotFound()
682 684 return PullRequestModel().generate_repo_data(
683 685 repo, translator=self.request.translate)
684 686
685 687 @LoginRequired()
686 688 @NotAnonymous()
687 689 @HasRepoPermissionAnyDecorator(
688 690 'repository.read', 'repository.write', 'repository.admin')
689 691 @view_config(
690 692 route_name='pullrequest_repo_destinations', request_method='GET',
691 693 renderer='json_ext', xhr=True)
692 694 def pull_request_repo_destinations(self):
693 695 _ = self.request.translate
694 696 filter_query = self.request.GET.get('query')
695 697
696 698 query = Repository.query() \
697 699 .order_by(func.length(Repository.repo_name)) \
698 700 .filter(
699 701 or_(Repository.repo_name == self.db_repo.repo_name,
700 702 Repository.fork_id == self.db_repo.repo_id))
701 703
702 704 if filter_query:
703 705 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
704 706 query = query.filter(
705 707 Repository.repo_name.ilike(ilike_expression))
706 708
707 709 add_parent = False
708 710 if self.db_repo.parent:
709 711 if filter_query in self.db_repo.parent.repo_name:
710 712 parent_vcs_obj = self.db_repo.parent.scm_instance()
711 713 if parent_vcs_obj and not parent_vcs_obj.is_empty():
712 714 add_parent = True
713 715
714 716 limit = 20 - 1 if add_parent else 20
715 717 all_repos = query.limit(limit).all()
716 718 if add_parent:
717 719 all_repos += [self.db_repo.parent]
718 720
719 721 repos = []
720 722 for obj in ScmModel().get_repos(all_repos):
721 723 repos.append({
722 724 'id': obj['name'],
723 725 'text': obj['name'],
724 726 'type': 'repo',
725 727 'obj': obj['dbrepo']
726 728 })
727 729
728 730 data = {
729 731 'more': False,
730 732 'results': [{
731 733 'text': _('Repositories'),
732 734 'children': repos
733 735 }] if repos else []
734 736 }
735 737 return data
736 738
737 739 @LoginRequired()
738 740 @NotAnonymous()
739 741 @HasRepoPermissionAnyDecorator(
740 742 'repository.read', 'repository.write', 'repository.admin')
741 743 @CSRFRequired()
742 744 @view_config(
743 745 route_name='pullrequest_create', request_method='POST',
744 746 renderer=None)
745 747 def pull_request_create(self):
746 748 _ = self.request.translate
747 749 self.assure_not_empty_repo()
750 self.load_default_context()
748 751
749 752 controls = peppercorn.parse(self.request.POST.items())
750 753
751 754 try:
752 755 form = PullRequestForm(
753 756 self.request.translate, self.db_repo.repo_id)()
754 757 _form = form.to_python(controls)
755 758 except formencode.Invalid as errors:
756 759 if errors.error_dict.get('revisions'):
757 760 msg = 'Revisions: %s' % errors.error_dict['revisions']
758 761 elif errors.error_dict.get('pullrequest_title'):
759 762 msg = _('Pull request requires a title with min. 3 chars')
760 763 else:
761 764 msg = _('Error creating pull request: {}').format(errors)
762 765 log.exception(msg)
763 766 h.flash(msg, 'error')
764 767
765 768 # would rather just go back to form ...
766 769 raise HTTPFound(
767 770 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
768 771
769 772 source_repo = _form['source_repo']
770 773 source_ref = _form['source_ref']
771 774 target_repo = _form['target_repo']
772 775 target_ref = _form['target_ref']
773 776 commit_ids = _form['revisions'][::-1]
774 777
775 778 # find the ancestor for this pr
776 779 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
777 780 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
778 781
779 782 # re-check permissions again here
780 783 # source_repo we must have read permissions
781 784
782 785 source_perm = HasRepoPermissionAny(
783 786 'repository.read',
784 787 'repository.write', 'repository.admin')(source_db_repo.repo_name)
785 788 if not source_perm:
786 789 msg = _('Not Enough permissions to source repo `{}`.'.format(
787 790 source_db_repo.repo_name))
788 791 h.flash(msg, category='error')
789 792 # copy the args back to redirect
790 793 org_query = self.request.GET.mixed()
791 794 raise HTTPFound(
792 795 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
793 796 _query=org_query))
794 797
795 798 # target repo we must have read permissions, and also later on
796 799 # we want to check branch permissions here
797 800 target_perm = HasRepoPermissionAny(
798 801 'repository.read',
799 802 'repository.write', 'repository.admin')(target_db_repo.repo_name)
800 803 if not target_perm:
801 804 msg = _('Not Enough permissions to target repo `{}`.'.format(
802 805 target_db_repo.repo_name))
803 806 h.flash(msg, category='error')
804 807 # copy the args back to redirect
805 808 org_query = self.request.GET.mixed()
806 809 raise HTTPFound(
807 810 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
808 811 _query=org_query))
809 812
810 813 source_scm = source_db_repo.scm_instance()
811 814 target_scm = target_db_repo.scm_instance()
812 815
813 816 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
814 817 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
815 818
816 819 ancestor = source_scm.get_common_ancestor(
817 820 source_commit.raw_id, target_commit.raw_id, target_scm)
818 821
819 822 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
820 823 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
821 824
822 825 pullrequest_title = _form['pullrequest_title']
823 826 title_source_ref = source_ref.split(':', 2)[1]
824 827 if not pullrequest_title:
825 828 pullrequest_title = PullRequestModel().generate_pullrequest_title(
826 829 source=source_repo,
827 830 source_ref=title_source_ref,
828 831 target=target_repo
829 832 )
830 833
831 834 description = _form['pullrequest_desc']
832 835
833 836 get_default_reviewers_data, validate_default_reviewers = \
834 837 PullRequestModel().get_reviewer_functions()
835 838
836 839 # recalculate reviewers logic, to make sure we can validate this
837 840 reviewer_rules = get_default_reviewers_data(
838 841 self._rhodecode_db_user, source_db_repo,
839 842 source_commit, target_db_repo, target_commit)
840 843
841 844 given_reviewers = _form['review_members']
842 845 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
843 846
844 847 try:
845 848 pull_request = PullRequestModel().create(
846 849 self._rhodecode_user.user_id, source_repo, source_ref,
847 850 target_repo, target_ref, commit_ids, reviewers,
848 851 pullrequest_title, description, reviewer_rules
849 852 )
850 853 Session().commit()
851 854
852 855 h.flash(_('Successfully opened new pull request'),
853 856 category='success')
854 857 except Exception:
855 858 msg = _('Error occurred during creation of this pull request.')
856 859 log.exception(msg)
857 860 h.flash(msg, category='error')
858 861
859 862 # copy the args back to redirect
860 863 org_query = self.request.GET.mixed()
861 864 raise HTTPFound(
862 865 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
863 866 _query=org_query))
864 867
865 868 raise HTTPFound(
866 869 h.route_path('pullrequest_show', repo_name=target_repo,
867 870 pull_request_id=pull_request.pull_request_id))
868 871
869 872 @LoginRequired()
870 873 @NotAnonymous()
871 874 @HasRepoPermissionAnyDecorator(
872 875 'repository.read', 'repository.write', 'repository.admin')
873 876 @CSRFRequired()
874 877 @view_config(
875 878 route_name='pullrequest_update', request_method='POST',
876 879 renderer='json_ext')
877 880 def pull_request_update(self):
878 881 pull_request = PullRequest.get_or_404(
879 882 self.request.matchdict['pull_request_id'])
880 883
884 self.load_default_context()
881 885 # only owner or admin can update it
882 886 allowed_to_update = PullRequestModel().check_user_update(
883 887 pull_request, self._rhodecode_user)
884 888 if allowed_to_update:
885 889 controls = peppercorn.parse(self.request.POST.items())
886 890
887 891 if 'review_members' in controls:
888 892 self._update_reviewers(
889 893 pull_request, controls['review_members'],
890 894 pull_request.reviewer_data)
891 895 elif str2bool(self.request.POST.get('update_commits', 'false')):
892 896 self._update_commits(pull_request)
893 897 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
894 898 self._edit_pull_request(pull_request)
895 899 else:
896 900 raise HTTPBadRequest()
897 901 return True
898 902 raise HTTPForbidden()
899 903
900 904 def _edit_pull_request(self, pull_request):
901 905 _ = self.request.translate
902 906 try:
903 907 PullRequestModel().edit(
904 908 pull_request, self.request.POST.get('title'),
905 909 self.request.POST.get('description'), self._rhodecode_user)
906 910 except ValueError:
907 911 msg = _(u'Cannot update closed pull requests.')
908 912 h.flash(msg, category='error')
909 913 return
910 914 else:
911 915 Session().commit()
912 916
913 917 msg = _(u'Pull request title & description updated.')
914 918 h.flash(msg, category='success')
915 919 return
916 920
917 921 def _update_commits(self, pull_request):
918 922 _ = self.request.translate
919 923 resp = PullRequestModel().update_commits(pull_request)
920 924
921 925 if resp.executed:
922 926
923 927 if resp.target_changed and resp.source_changed:
924 928 changed = 'target and source repositories'
925 929 elif resp.target_changed and not resp.source_changed:
926 930 changed = 'target repository'
927 931 elif not resp.target_changed and resp.source_changed:
928 932 changed = 'source repository'
929 933 else:
930 934 changed = 'nothing'
931 935
932 936 msg = _(
933 937 u'Pull request updated to "{source_commit_id}" with '
934 938 u'{count_added} added, {count_removed} removed commits. '
935 939 u'Source of changes: {change_source}')
936 940 msg = msg.format(
937 941 source_commit_id=pull_request.source_ref_parts.commit_id,
938 942 count_added=len(resp.changes.added),
939 943 count_removed=len(resp.changes.removed),
940 944 change_source=changed)
941 945 h.flash(msg, category='success')
942 946
943 947 channel = '/repo${}$/pr/{}'.format(
944 948 pull_request.target_repo.repo_name,
945 949 pull_request.pull_request_id)
946 950 message = msg + (
947 951 ' - <a onclick="window.location.reload()">'
948 952 '<strong>{}</strong></a>'.format(_('Reload page')))
949 953 channelstream.post_message(
950 954 channel, message, self._rhodecode_user.username,
951 955 registry=self.request.registry)
952 956 else:
953 957 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
954 958 warning_reasons = [
955 959 UpdateFailureReason.NO_CHANGE,
956 960 UpdateFailureReason.WRONG_REF_TYPE,
957 961 ]
958 962 category = 'warning' if resp.reason in warning_reasons else 'error'
959 963 h.flash(msg, category=category)
960 964
961 965 @LoginRequired()
962 966 @NotAnonymous()
963 967 @HasRepoPermissionAnyDecorator(
964 968 'repository.read', 'repository.write', 'repository.admin')
965 969 @CSRFRequired()
966 970 @view_config(
967 971 route_name='pullrequest_merge', request_method='POST',
968 972 renderer='json_ext')
969 973 def pull_request_merge(self):
970 974 """
971 975 Merge will perform a server-side merge of the specified
972 976 pull request, if the pull request is approved and mergeable.
973 977 After successful merging, the pull request is automatically
974 978 closed, with a relevant comment.
975 979 """
976 980 pull_request = PullRequest.get_or_404(
977 981 self.request.matchdict['pull_request_id'])
978 982
983 self.load_default_context()
979 984 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
980 985 translator=self.request.translate)
981 986 merge_possible = not check.failed
982 987
983 988 for err_type, error_msg in check.errors:
984 989 h.flash(error_msg, category=err_type)
985 990
986 991 if merge_possible:
987 992 log.debug("Pre-conditions checked, trying to merge.")
988 993 extras = vcs_operation_context(
989 994 self.request.environ, repo_name=pull_request.target_repo.repo_name,
990 995 username=self._rhodecode_db_user.username, action='push',
991 996 scm=pull_request.target_repo.repo_type)
992 997 self._merge_pull_request(
993 998 pull_request, self._rhodecode_db_user, extras)
994 999 else:
995 1000 log.debug("Pre-conditions failed, NOT merging.")
996 1001
997 1002 raise HTTPFound(
998 1003 h.route_path('pullrequest_show',
999 1004 repo_name=pull_request.target_repo.repo_name,
1000 1005 pull_request_id=pull_request.pull_request_id))
1001 1006
1002 1007 def _merge_pull_request(self, pull_request, user, extras):
1003 1008 _ = self.request.translate
1004 1009 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1005 1010
1006 1011 if merge_resp.executed:
1007 1012 log.debug("The merge was successful, closing the pull request.")
1008 1013 PullRequestModel().close_pull_request(
1009 1014 pull_request.pull_request_id, user)
1010 1015 Session().commit()
1011 1016 msg = _('Pull request was successfully merged and closed.')
1012 1017 h.flash(msg, category='success')
1013 1018 else:
1014 1019 log.debug(
1015 1020 "The merge was not successful. Merge response: %s",
1016 1021 merge_resp)
1017 1022 msg = PullRequestModel().merge_status_message(
1018 1023 merge_resp.failure_reason)
1019 1024 h.flash(msg, category='error')
1020 1025
1021 1026 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1022 1027 _ = self.request.translate
1023 1028 get_default_reviewers_data, validate_default_reviewers = \
1024 1029 PullRequestModel().get_reviewer_functions()
1025 1030
1026 1031 try:
1027 1032 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1028 1033 except ValueError as e:
1029 1034 log.error('Reviewers Validation: {}'.format(e))
1030 1035 h.flash(e, category='error')
1031 1036 return
1032 1037
1033 1038 PullRequestModel().update_reviewers(
1034 1039 pull_request, reviewers, self._rhodecode_user)
1035 1040 h.flash(_('Pull request reviewers updated.'), category='success')
1036 1041 Session().commit()
1037 1042
1038 1043 @LoginRequired()
1039 1044 @NotAnonymous()
1040 1045 @HasRepoPermissionAnyDecorator(
1041 1046 'repository.read', 'repository.write', 'repository.admin')
1042 1047 @CSRFRequired()
1043 1048 @view_config(
1044 1049 route_name='pullrequest_delete', request_method='POST',
1045 1050 renderer='json_ext')
1046 1051 def pull_request_delete(self):
1047 1052 _ = self.request.translate
1048 1053
1049 1054 pull_request = PullRequest.get_or_404(
1050 1055 self.request.matchdict['pull_request_id'])
1056 self.load_default_context()
1051 1057
1052 1058 pr_closed = pull_request.is_closed()
1053 1059 allowed_to_delete = PullRequestModel().check_user_delete(
1054 1060 pull_request, self._rhodecode_user) and not pr_closed
1055 1061
1056 1062 # only owner can delete it !
1057 1063 if allowed_to_delete:
1058 1064 PullRequestModel().delete(pull_request, self._rhodecode_user)
1059 1065 Session().commit()
1060 1066 h.flash(_('Successfully deleted pull request'),
1061 1067 category='success')
1062 1068 raise HTTPFound(h.route_path('pullrequest_show_all',
1063 1069 repo_name=self.db_repo_name))
1064 1070
1065 1071 log.warning('user %s tried to delete pull request without access',
1066 1072 self._rhodecode_user)
1067 1073 raise HTTPNotFound()
1068 1074
1069 1075 @LoginRequired()
1070 1076 @NotAnonymous()
1071 1077 @HasRepoPermissionAnyDecorator(
1072 1078 'repository.read', 'repository.write', 'repository.admin')
1073 1079 @CSRFRequired()
1074 1080 @view_config(
1075 1081 route_name='pullrequest_comment_create', request_method='POST',
1076 1082 renderer='json_ext')
1077 1083 def pull_request_comment_create(self):
1078 1084 _ = self.request.translate
1079 1085
1080 1086 pull_request = PullRequest.get_or_404(
1081 1087 self.request.matchdict['pull_request_id'])
1082 1088 pull_request_id = pull_request.pull_request_id
1083 1089
1084 1090 if pull_request.is_closed():
1085 1091 log.debug('comment: forbidden because pull request is closed')
1086 1092 raise HTTPForbidden()
1087 1093
1088 1094 allowed_to_comment = PullRequestModel().check_user_comment(
1089 1095 pull_request, self._rhodecode_user)
1090 1096 if not allowed_to_comment:
1091 1097 log.debug(
1092 1098 'comment: forbidden because pull request is from forbidden repo')
1093 1099 raise HTTPForbidden()
1094 1100
1095 1101 c = self.load_default_context()
1096 1102
1097 1103 status = self.request.POST.get('changeset_status', None)
1098 1104 text = self.request.POST.get('text')
1099 1105 comment_type = self.request.POST.get('comment_type')
1100 1106 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1101 1107 close_pull_request = self.request.POST.get('close_pull_request')
1102 1108
1103 1109 # the logic here should work like following, if we submit close
1104 1110 # pr comment, use `close_pull_request_with_comment` function
1105 1111 # else handle regular comment logic
1106 1112
1107 1113 if close_pull_request:
1108 1114 # only owner or admin or person with write permissions
1109 1115 allowed_to_close = PullRequestModel().check_user_update(
1110 1116 pull_request, self._rhodecode_user)
1111 1117 if not allowed_to_close:
1112 1118 log.debug('comment: forbidden because not allowed to close '
1113 1119 'pull request %s', pull_request_id)
1114 1120 raise HTTPForbidden()
1115 1121 comment, status = PullRequestModel().close_pull_request_with_comment(
1116 1122 pull_request, self._rhodecode_user, self.db_repo, message=text)
1117 1123 Session().flush()
1118 1124 events.trigger(
1119 1125 events.PullRequestCommentEvent(pull_request, comment))
1120 1126
1121 1127 else:
1122 1128 # regular comment case, could be inline, or one with status.
1123 1129 # for that one we check also permissions
1124 1130
1125 1131 allowed_to_change_status = PullRequestModel().check_user_change_status(
1126 1132 pull_request, self._rhodecode_user)
1127 1133
1128 1134 if status and allowed_to_change_status:
1129 1135 message = (_('Status change %(transition_icon)s %(status)s')
1130 1136 % {'transition_icon': '>',
1131 1137 'status': ChangesetStatus.get_status_lbl(status)})
1132 1138 text = text or message
1133 1139
1134 1140 comment = CommentsModel().create(
1135 1141 text=text,
1136 1142 repo=self.db_repo.repo_id,
1137 1143 user=self._rhodecode_user.user_id,
1138 1144 pull_request=pull_request,
1139 1145 f_path=self.request.POST.get('f_path'),
1140 1146 line_no=self.request.POST.get('line'),
1141 1147 status_change=(ChangesetStatus.get_status_lbl(status)
1142 1148 if status and allowed_to_change_status else None),
1143 1149 status_change_type=(status
1144 1150 if status and allowed_to_change_status else None),
1145 1151 comment_type=comment_type,
1146 1152 resolves_comment_id=resolves_comment_id
1147 1153 )
1148 1154
1149 1155 if allowed_to_change_status:
1150 1156 # calculate old status before we change it
1151 1157 old_calculated_status = pull_request.calculated_review_status()
1152 1158
1153 1159 # get status if set !
1154 1160 if status:
1155 1161 ChangesetStatusModel().set_status(
1156 1162 self.db_repo.repo_id,
1157 1163 status,
1158 1164 self._rhodecode_user.user_id,
1159 1165 comment,
1160 1166 pull_request=pull_request
1161 1167 )
1162 1168
1163 1169 Session().flush()
1164 1170 events.trigger(
1165 1171 events.PullRequestCommentEvent(pull_request, comment))
1166 1172
1167 1173 # we now calculate the status of pull request, and based on that
1168 1174 # calculation we set the commits status
1169 1175 calculated_status = pull_request.calculated_review_status()
1170 1176 if old_calculated_status != calculated_status:
1171 1177 PullRequestModel()._trigger_pull_request_hook(
1172 1178 pull_request, self._rhodecode_user, 'review_status_change')
1173 1179
1174 1180 Session().commit()
1175 1181
1176 1182 data = {
1177 1183 'target_id': h.safeid(h.safe_unicode(
1178 1184 self.request.POST.get('f_path'))),
1179 1185 }
1180 1186 if comment:
1181 1187 c.co = comment
1182 1188 rendered_comment = render(
1183 1189 'rhodecode:templates/changeset/changeset_comment_block.mako',
1184 1190 self._get_template_context(c), self.request)
1185 1191
1186 1192 data.update(comment.get_dict())
1187 1193 data.update({'rendered_text': rendered_comment})
1188 1194
1189 1195 return data
1190 1196
1191 1197 @LoginRequired()
1192 1198 @NotAnonymous()
1193 1199 @HasRepoPermissionAnyDecorator(
1194 1200 'repository.read', 'repository.write', 'repository.admin')
1195 1201 @CSRFRequired()
1196 1202 @view_config(
1197 1203 route_name='pullrequest_comment_delete', request_method='POST',
1198 1204 renderer='json_ext')
1199 1205 def pull_request_comment_delete(self):
1200 1206 pull_request = PullRequest.get_or_404(
1201 1207 self.request.matchdict['pull_request_id'])
1202 1208
1203 1209 comment = ChangesetComment.get_or_404(
1204 1210 self.request.matchdict['comment_id'])
1205 1211 comment_id = comment.comment_id
1206 1212
1207 1213 if pull_request.is_closed():
1208 1214 log.debug('comment: forbidden because pull request is closed')
1209 1215 raise HTTPForbidden()
1210 1216
1211 1217 if not comment:
1212 1218 log.debug('Comment with id:%s not found, skipping', comment_id)
1213 1219 # comment already deleted in another call probably
1214 1220 return True
1215 1221
1216 1222 if comment.pull_request.is_closed():
1217 1223 # don't allow deleting comments on closed pull request
1218 1224 raise HTTPForbidden()
1219 1225
1220 1226 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1221 1227 super_admin = h.HasPermissionAny('hg.admin')()
1222 1228 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1223 1229 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1224 1230 comment_repo_admin = is_repo_admin and is_repo_comment
1225 1231
1226 1232 if super_admin or comment_owner or comment_repo_admin:
1227 1233 old_calculated_status = comment.pull_request.calculated_review_status()
1228 1234 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1229 1235 Session().commit()
1230 1236 calculated_status = comment.pull_request.calculated_review_status()
1231 1237 if old_calculated_status != calculated_status:
1232 1238 PullRequestModel()._trigger_pull_request_hook(
1233 1239 comment.pull_request, self._rhodecode_user, 'review_status_change')
1234 1240 return True
1235 1241 else:
1236 1242 log.warning('No permissions for user %s to delete comment_id: %s',
1237 1243 self._rhodecode_db_user, comment_id)
1238 1244 raise HTTPNotFound()
@@ -1,251 +1,252 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, CSRFRequired)
33 33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.repo import RepoModel
36 36 from rhodecode.model.scm import RepoGroupList, ScmModel
37 37 from rhodecode.model.validation_schema.schemas import repo_schema
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class RepoSettingsView(RepoAppView):
43 43
44 44 def load_default_context(self):
45 45 c = self._get_local_tmpl_context()
46 46
47 47 acl_groups = RepoGroupList(
48 48 RepoGroup.query().all(),
49 49 perm_set=['group.write', 'group.admin'])
50 50 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
51 51 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
52 52
53 53 # in case someone no longer have a group.write access to a repository
54 54 # pre fill the list with this entry, we don't care if this is the same
55 55 # but it will allow saving repo data properly.
56 56 repo_group = self.db_repo.group
57 57 if repo_group and repo_group.group_id not in c.repo_groups_choices:
58 58 c.repo_groups_choices.append(repo_group.group_id)
59 59 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
60 60
61 61 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
62 62 # we might be in missing requirement state, so we load things
63 63 # without touching scm_instance()
64 64 c.landing_revs_choices, c.landing_revs = \
65 ScmModel().get_repo_landing_revs()
65 ScmModel().get_repo_landing_revs(self.request.translate)
66 66 else:
67 67 c.landing_revs_choices, c.landing_revs = \
68 ScmModel().get_repo_landing_revs(self.db_repo)
68 ScmModel().get_repo_landing_revs(
69 self.request.translate, self.db_repo)
69 70
70 71 c.personal_repo_group = c.auth_user.personal_repo_group
71 72 c.repo_fields = RepositoryField.query()\
72 73 .filter(RepositoryField.repository == self.db_repo).all()
73 74
74 75
75 76 return c
76 77
77 78 def _get_schema(self, c, old_values=None):
78 79 return repo_schema.RepoSettingsSchema().bind(
79 80 repo_type=self.db_repo.repo_type,
80 81 repo_type_options=[self.db_repo.repo_type],
81 82 repo_ref_options=c.landing_revs_choices,
82 83 repo_ref_items=c.landing_revs,
83 84 repo_repo_group_options=c.repo_groups_choices,
84 85 repo_repo_group_items=c.repo_groups,
85 86 # user caller
86 87 user=self._rhodecode_user,
87 88 old_values=old_values
88 89 )
89 90
90 91 @LoginRequired()
91 92 @HasRepoPermissionAnyDecorator('repository.admin')
92 93 @view_config(
93 94 route_name='edit_repo', request_method='GET',
94 95 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
95 96 def edit_settings(self):
96 97 c = self.load_default_context()
97 98 c.active = 'settings'
98 99
99 100 defaults = RepoModel()._get_defaults(self.db_repo_name)
100 101 defaults['repo_owner'] = defaults['user']
101 102 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
102 103
103 104 schema = self._get_schema(c)
104 105 c.form = RcForm(schema, appstruct=defaults)
105 106 return self._get_template_context(c)
106 107
107 108 @LoginRequired()
108 109 @HasRepoPermissionAnyDecorator('repository.admin')
109 110 @CSRFRequired()
110 111 @view_config(
111 112 route_name='edit_repo', request_method='POST',
112 113 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
113 114 def edit_settings_update(self):
114 115 _ = self.request.translate
115 116 c = self.load_default_context()
116 117 c.active = 'settings'
117 118 old_repo_name = self.db_repo_name
118 119
119 120 old_values = self.db_repo.get_api_data()
120 121 schema = self._get_schema(c, old_values=old_values)
121 122
122 123 c.form = RcForm(schema)
123 124 pstruct = self.request.POST.items()
124 125 pstruct.append(('repo_type', self.db_repo.repo_type))
125 126 try:
126 127 schema_data = c.form.validate(pstruct)
127 128 except deform.ValidationFailure as err_form:
128 129 return self._get_template_context(c)
129 130
130 131 # data is now VALID, proceed with updates
131 132 # save validated data back into the updates dict
132 133 validated_updates = dict(
133 134 repo_name=schema_data['repo_group']['repo_name_without_group'],
134 135 repo_group=schema_data['repo_group']['repo_group_id'],
135 136
136 137 user=schema_data['repo_owner'],
137 138 repo_description=schema_data['repo_description'],
138 139 repo_private=schema_data['repo_private'],
139 140 clone_uri=schema_data['repo_clone_uri'],
140 141 repo_landing_rev=schema_data['repo_landing_commit_ref'],
141 142 repo_enable_statistics=schema_data['repo_enable_statistics'],
142 143 repo_enable_locking=schema_data['repo_enable_locking'],
143 144 repo_enable_downloads=schema_data['repo_enable_downloads'],
144 145 )
145 146 # detect if CLONE URI changed, if we get OLD means we keep old values
146 147 if schema_data['repo_clone_uri_change'] == 'OLD':
147 148 validated_updates['clone_uri'] = self.db_repo.clone_uri
148 149
149 150 # use the new full name for redirect
150 151 new_repo_name = schema_data['repo_group']['repo_name_with_group']
151 152
152 153 # save extra fields into our validated data
153 154 for key, value in pstruct:
154 155 if key.startswith(RepositoryField.PREFIX):
155 156 validated_updates[key] = value
156 157
157 158 try:
158 159 RepoModel().update(self.db_repo, **validated_updates)
159 160 ScmModel().mark_for_invalidation(new_repo_name)
160 161
161 162 audit_logger.store_web(
162 163 'repo.edit', action_data={'old_data': old_values},
163 164 user=self._rhodecode_user, repo=self.db_repo)
164 165
165 166 Session().commit()
166 167
167 168 h.flash(_('Repository `{}` updated successfully').format(
168 169 old_repo_name), category='success')
169 170 except Exception:
170 171 log.exception("Exception during update of repository")
171 172 h.flash(_('Error occurred during update of repository {}').format(
172 173 old_repo_name), category='error')
173 174
174 175 raise HTTPFound(
175 176 h.route_path('edit_repo', repo_name=new_repo_name))
176 177
177 178 @LoginRequired()
178 179 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
179 180 @view_config(
180 181 route_name='repo_edit_toggle_locking', request_method='GET',
181 182 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
182 183 def toggle_locking(self):
183 184 """
184 185 Toggle locking of repository by simple GET call to url
185 186 """
186 187 _ = self.request.translate
187 188 repo = self.db_repo
188 189
189 190 try:
190 191 if repo.enable_locking:
191 192 if repo.locked[0]:
192 193 Repository.unlock(repo)
193 194 action = _('Unlocked')
194 195 else:
195 196 Repository.lock(
196 197 repo, self._rhodecode_user.user_id,
197 198 lock_reason=Repository.LOCK_WEB)
198 199 action = _('Locked')
199 200
200 201 h.flash(_('Repository has been %s') % action,
201 202 category='success')
202 203 except Exception:
203 204 log.exception("Exception during unlocking")
204 205 h.flash(_('An error occurred during unlocking'),
205 206 category='error')
206 207 raise HTTPFound(
207 208 h.route_path('repo_summary', repo_name=self.db_repo_name))
208 209
209 210 @LoginRequired()
210 211 @HasRepoPermissionAnyDecorator('repository.admin')
211 212 @view_config(
212 213 route_name='edit_repo_statistics', request_method='GET',
213 214 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
214 215 def edit_statistics_form(self):
215 216 c = self.load_default_context()
216 217
217 218 if self.db_repo.stats:
218 219 # this is on what revision we ended up so we add +1 for count
219 220 last_rev = self.db_repo.stats.stat_on_revision + 1
220 221 else:
221 222 last_rev = 0
222 223
223 224 c.active = 'statistics'
224 225 c.stats_revision = last_rev
225 226 c.repo_last_rev = self.rhodecode_vcs_repo.count()
226 227
227 228 if last_rev == 0 or c.repo_last_rev == 0:
228 229 c.stats_percentage = 0
229 230 else:
230 231 c.stats_percentage = '%.2f' % (
231 232 (float((last_rev)) / c.repo_last_rev) * 100)
232 233 return self._get_template_context(c)
233 234
234 235 @LoginRequired()
235 236 @HasRepoPermissionAnyDecorator('repository.admin')
236 237 @CSRFRequired()
237 238 @view_config(
238 239 route_name='edit_repo_statistics_reset', request_method='POST',
239 240 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
240 241 def repo_statistics_reset(self):
241 242 _ = self.request.translate
242 243
243 244 try:
244 245 RepoModel().delete_stats(self.db_repo_name)
245 246 Session().commit()
246 247 except Exception:
247 248 log.exception('Edit statistics failure')
248 249 h.flash(_('An error occurred during deletion of repository stats'),
249 250 category='error')
250 251 raise HTTPFound(
251 252 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
@@ -1,546 +1,546 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import peppercorn
24 24 import formencode
25 25 import formencode.htmlfill
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode.lib.exceptions import (
32 32 RepoGroupAssignmentError, UserGroupAssignedException)
33 33 from rhodecode.model.forms import (
34 34 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
35 35 UserPermissionsForm)
36 36 from rhodecode.model.permission import PermissionModel
37 37
38 38 from rhodecode.apps._base import UserGroupAppView
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
41 41 from rhodecode.lib import helpers as h, audit_logger
42 42 from rhodecode.lib.utils2 import str2bool
43 43 from rhodecode.model.db import (
44 44 joinedload, User, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class UserGroupsView(UserGroupAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55
56 56 PermissionModel().set_global_permission_choices(
57 57 c, gettext_translator=self.request.translate)
58 58
59 59
60 60 return c
61 61
62 62 def _get_perms_summary(self, user_group_id):
63 63 permissions = {
64 64 'repositories': {},
65 65 'repositories_groups': {},
66 66 }
67 67 ugroup_repo_perms = UserGroupRepoToPerm.query()\
68 68 .options(joinedload(UserGroupRepoToPerm.permission))\
69 69 .options(joinedload(UserGroupRepoToPerm.repository))\
70 70 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
71 71 .all()
72 72
73 73 for gr in ugroup_repo_perms:
74 74 permissions['repositories'][gr.repository.repo_name] \
75 75 = gr.permission.permission_name
76 76
77 77 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
78 78 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
79 79 .options(joinedload(UserGroupRepoGroupToPerm.group))\
80 80 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
81 81 .all()
82 82
83 83 for gr in ugroup_group_perms:
84 84 permissions['repositories_groups'][gr.group.group_name] \
85 85 = gr.permission.permission_name
86 86 return permissions
87 87
88 88 @LoginRequired()
89 89 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
90 90 @view_config(
91 91 route_name='user_group_members_data', request_method='GET',
92 92 renderer='json_ext', xhr=True)
93 93 def user_group_members(self):
94 94 """
95 95 Return members of given user group
96 96 """
97 97 self.load_default_context()
98 98 user_group = self.db_user_group
99 99 group_members_obj = sorted((x.user for x in user_group.members),
100 100 key=lambda u: u.username.lower())
101 101
102 102 group_members = [
103 103 {
104 104 'id': user.user_id,
105 105 'first_name': user.first_name,
106 106 'last_name': user.last_name,
107 107 'username': user.username,
108 108 'icon_link': h.gravatar_url(user.email, 30),
109 109 'value_display': h.person(user.email),
110 110 'value': user.username,
111 111 'value_type': 'user',
112 112 'active': user.active,
113 113 }
114 114 for user in group_members_obj
115 115 ]
116 116
117 117 return {
118 118 'members': group_members
119 119 }
120 120
121 121 @LoginRequired()
122 122 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
123 123 @view_config(
124 124 route_name='edit_user_group_perms_summary', request_method='GET',
125 125 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
126 126 def user_group_perms_summary(self):
127 127 c = self.load_default_context()
128 128 c.user_group = self.db_user_group
129 129 c.active = 'perms_summary'
130 130 c.permissions = self._get_perms_summary(c.user_group.users_group_id)
131 131 return self._get_template_context(c)
132 132
133 133 @LoginRequired()
134 134 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
135 135 @view_config(
136 136 route_name='edit_user_group_perms_summary_json', request_method='GET',
137 137 renderer='json_ext')
138 138 def user_group_perms_summary_json(self):
139 139 self.load_default_context()
140 140 user_group = self.db_user_group
141 141 return self._get_perms_summary(user_group.users_group_id)
142 142
143 143 def _revoke_perms_on_yourself(self, form_result):
144 144 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
145 145 form_result['perm_updates'])
146 146 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
147 147 form_result['perm_additions'])
148 148 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
149 149 form_result['perm_deletions'])
150 150 admin_perm = 'usergroup.admin'
151 151 if _updates and _updates[0][1] != admin_perm or \
152 152 _additions and _additions[0][1] != admin_perm or \
153 153 _deletions and _deletions[0][1] != admin_perm:
154 154 return True
155 155 return False
156 156
157 157 @LoginRequired()
158 158 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
159 159 @CSRFRequired()
160 160 @view_config(
161 161 route_name='user_groups_update', request_method='POST',
162 162 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
163 163 def user_group_update(self):
164 164 _ = self.request.translate
165 165
166 166 user_group = self.db_user_group
167 167 user_group_id = user_group.users_group_id
168 168
169 169 c = self.load_default_context()
170 170 c.user_group = user_group
171 171 c.group_members_obj = [x.user for x in c.user_group.members]
172 172 c.group_members_obj.sort(key=lambda u: u.username.lower())
173 173 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
174 174 c.active = 'settings'
175 175
176 176 users_group_form = UserGroupForm(
177 177 self.request.translate, edit=True,
178 178 old_data=c.user_group.get_dict(), allow_disabled=True)()
179 179
180 180 old_values = c.user_group.get_api_data()
181 181 user_group_name = self.request.POST.get('users_group_name')
182 182 try:
183 183 form_result = users_group_form.to_python(self.request.POST)
184 184 pstruct = peppercorn.parse(self.request.POST.items())
185 185 form_result['users_group_members'] = pstruct['user_group_members']
186 186
187 187 user_group, added_members, removed_members = \
188 188 UserGroupModel().update(c.user_group, form_result)
189 189 updated_user_group = form_result['users_group_name']
190 190
191 191 for user_id in added_members:
192 192 user = User.get(user_id)
193 193 user_data = user.get_api_data()
194 194 audit_logger.store_web(
195 195 'user_group.edit.member.add',
196 196 action_data={'user': user_data, 'old_data': old_values},
197 197 user=self._rhodecode_user)
198 198
199 199 for user_id in removed_members:
200 200 user = User.get(user_id)
201 201 user_data = user.get_api_data()
202 202 audit_logger.store_web(
203 203 'user_group.edit.member.delete',
204 204 action_data={'user': user_data, 'old_data': old_values},
205 205 user=self._rhodecode_user)
206 206
207 207 audit_logger.store_web(
208 208 'user_group.edit', action_data={'old_data': old_values},
209 209 user=self._rhodecode_user)
210 210
211 211 h.flash(_('Updated user group %s') % updated_user_group,
212 212 category='success')
213 213 Session().commit()
214 214 except formencode.Invalid as errors:
215 215 defaults = errors.value
216 216 e = errors.error_dict or {}
217 217
218 218 data = render(
219 219 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
220 220 self._get_template_context(c), self.request)
221 221 html = formencode.htmlfill.render(
222 222 data,
223 223 defaults=defaults,
224 224 errors=e,
225 225 prefix_error=False,
226 226 encoding="UTF-8",
227 227 force_defaults=False
228 228 )
229 229 return Response(html)
230 230
231 231 except Exception:
232 232 log.exception("Exception during update of user group")
233 233 h.flash(_('Error occurred during update of user group %s')
234 234 % user_group_name, category='error')
235 235
236 236 raise HTTPFound(
237 237 h.route_path('edit_user_group', user_group_id=user_group_id))
238 238
239 239 @LoginRequired()
240 240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
241 241 @CSRFRequired()
242 242 @view_config(
243 243 route_name='user_groups_delete', request_method='POST',
244 244 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
245 245 def user_group_delete(self):
246 246 _ = self.request.translate
247 247 user_group = self.db_user_group
248 248
249 249 self.load_default_context()
250 250 force = str2bool(self.request.POST.get('force'))
251 251
252 252 old_values = user_group.get_api_data()
253 253 try:
254 254 UserGroupModel().delete(user_group, force=force)
255 255 audit_logger.store_web(
256 256 'user.delete', action_data={'old_data': old_values},
257 257 user=self._rhodecode_user)
258 258 Session().commit()
259 259 h.flash(_('Successfully deleted user group'), category='success')
260 260 except UserGroupAssignedException as e:
261 261 h.flash(str(e), category='error')
262 262 except Exception:
263 263 log.exception("Exception during deletion of user group")
264 264 h.flash(_('An error occurred during deletion of user group'),
265 265 category='error')
266 266 raise HTTPFound(h.route_path('user_groups'))
267 267
268 268 @LoginRequired()
269 269 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
270 270 @view_config(
271 271 route_name='edit_user_group', request_method='GET',
272 272 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
273 273 def user_group_edit(self):
274 274 user_group = self.db_user_group
275 275
276 276 c = self.load_default_context()
277 277 c.user_group = user_group
278 278 c.group_members_obj = [x.user for x in c.user_group.members]
279 279 c.group_members_obj.sort(key=lambda u: u.username.lower())
280 280 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
281 281
282 282 c.active = 'settings'
283 283
284 284 defaults = user_group.get_dict()
285 285 # fill owner
286 286 if user_group.user:
287 287 defaults.update({'user': user_group.user.username})
288 288 else:
289 289 replacement_user = User.get_first_super_admin().username
290 290 defaults.update({'user': replacement_user})
291 291
292 292 data = render(
293 293 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
294 294 self._get_template_context(c), self.request)
295 295 html = formencode.htmlfill.render(
296 296 data,
297 297 defaults=defaults,
298 298 encoding="UTF-8",
299 299 force_defaults=False
300 300 )
301 301 return Response(html)
302 302
303 303 @LoginRequired()
304 304 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
305 305 @view_config(
306 306 route_name='edit_user_group_perms', request_method='GET',
307 307 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
308 308 def user_group_edit_perms(self):
309 309 user_group = self.db_user_group
310 310 c = self.load_default_context()
311 311 c.user_group = user_group
312 312 c.active = 'perms'
313 313
314 314 defaults = {}
315 315 # fill user group users
316 316 for p in c.user_group.user_user_group_to_perm:
317 317 defaults.update({'u_perm_%s' % p.user.user_id:
318 318 p.permission.permission_name})
319 319
320 320 for p in c.user_group.user_group_user_group_to_perm:
321 321 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
322 322 p.permission.permission_name})
323 323
324 324 data = render(
325 325 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
326 326 self._get_template_context(c), self.request)
327 327 html = formencode.htmlfill.render(
328 328 data,
329 329 defaults=defaults,
330 330 encoding="UTF-8",
331 331 force_defaults=False
332 332 )
333 333 return Response(html)
334 334
335 335 @LoginRequired()
336 336 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
337 337 @CSRFRequired()
338 338 @view_config(
339 339 route_name='edit_user_group_perms_update', request_method='POST',
340 340 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
341 341 def user_group_update_perms(self):
342 342 """
343 343 grant permission for given user group
344 344 """
345 345 _ = self.request.translate
346 346
347 347 user_group = self.db_user_group
348 348 user_group_id = user_group.users_group_id
349 349 c = self.load_default_context()
350 350 c.user_group = user_group
351 351 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
352 352
353 353 if not self._rhodecode_user.is_admin:
354 354 if self._revoke_perms_on_yourself(form):
355 355 msg = _('Cannot change permission for yourself as admin')
356 356 h.flash(msg, category='warning')
357 357 raise HTTPFound(
358 358 h.route_path('edit_user_group_perms',
359 359 user_group_id=user_group_id))
360 360
361 361 try:
362 362 changes = UserGroupModel().update_permissions(
363 363 user_group_id,
364 364 form['perm_additions'], form['perm_updates'],
365 365 form['perm_deletions'])
366 366
367 367 except RepoGroupAssignmentError:
368 368 h.flash(_('Target group cannot be the same'), category='error')
369 369 raise HTTPFound(
370 370 h.route_path('edit_user_group_perms',
371 371 user_group_id=user_group_id))
372 372
373 373 action_data = {
374 374 'added': changes['added'],
375 375 'updated': changes['updated'],
376 376 'deleted': changes['deleted'],
377 377 }
378 378 audit_logger.store_web(
379 379 'user_group.edit.permissions', action_data=action_data,
380 380 user=self._rhodecode_user)
381 381
382 382 Session().commit()
383 383 h.flash(_('User Group permissions updated'), category='success')
384 384 raise HTTPFound(
385 385 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
386 386
387 387 @LoginRequired()
388 388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
389 389 @view_config(
390 390 route_name='edit_user_group_global_perms', request_method='GET',
391 391 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
392 392 def user_group_global_perms_edit(self):
393 393 user_group = self.db_user_group
394 394 c = self.load_default_context()
395 395 c.user_group = user_group
396 396 c.active = 'global_perms'
397 397
398 398 c.default_user = User.get_default_user()
399 399 defaults = c.user_group.get_dict()
400 400 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
401 401 defaults.update(c.user_group.get_default_perms())
402 402
403 403 data = render(
404 404 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
405 405 self._get_template_context(c), self.request)
406 406 html = formencode.htmlfill.render(
407 407 data,
408 408 defaults=defaults,
409 409 encoding="UTF-8",
410 410 force_defaults=False
411 411 )
412 412 return Response(html)
413 413
414 414 @LoginRequired()
415 415 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
416 416 @CSRFRequired()
417 417 @view_config(
418 418 route_name='edit_user_group_global_perms_update', request_method='POST',
419 419 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
420 420 def user_group_global_perms_update(self):
421 421 _ = self.request.translate
422 422 user_group = self.db_user_group
423 423 user_group_id = self.db_user_group.users_group_id
424 424
425 425 c = self.load_default_context()
426 426 c.user_group = user_group
427 427 c.active = 'global_perms'
428 428
429 429 try:
430 430 # first stage that verifies the checkbox
431 _form = UserIndividualPermissionsForm()
431 _form = UserIndividualPermissionsForm(self.request.translate)
432 432 form_result = _form.to_python(dict(self.request.POST))
433 433 inherit_perms = form_result['inherit_default_permissions']
434 434 user_group.inherit_default_permissions = inherit_perms
435 435 Session().add(user_group)
436 436
437 437 if not inherit_perms:
438 438 # only update the individual ones if we un check the flag
439 439 _form = UserPermissionsForm(
440 440 self.request.translate,
441 441 [x[0] for x in c.repo_create_choices],
442 442 [x[0] for x in c.repo_create_on_write_choices],
443 443 [x[0] for x in c.repo_group_create_choices],
444 444 [x[0] for x in c.user_group_create_choices],
445 445 [x[0] for x in c.fork_choices],
446 446 [x[0] for x in c.inherit_default_permission_choices])()
447 447
448 448 form_result = _form.to_python(dict(self.request.POST))
449 449 form_result.update(
450 450 {'perm_user_group_id': user_group.users_group_id})
451 451
452 452 PermissionModel().update_user_group_permissions(form_result)
453 453
454 454 Session().commit()
455 455 h.flash(_('User Group global permissions updated successfully'),
456 456 category='success')
457 457
458 458 except formencode.Invalid as errors:
459 459 defaults = errors.value
460 460
461 461 data = render(
462 462 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
463 463 self._get_template_context(c), self.request)
464 464 html = formencode.htmlfill.render(
465 465 data,
466 466 defaults=defaults,
467 467 errors=errors.error_dict or {},
468 468 prefix_error=False,
469 469 encoding="UTF-8",
470 470 force_defaults=False
471 471 )
472 472 return Response(html)
473 473 except Exception:
474 474 log.exception("Exception during permissions saving")
475 475 h.flash(_('An error occurred during permissions saving'),
476 476 category='error')
477 477
478 478 raise HTTPFound(
479 479 h.route_path('edit_user_group_global_perms',
480 480 user_group_id=user_group_id))
481 481
482 482 @LoginRequired()
483 483 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
484 484 @view_config(
485 485 route_name='edit_user_group_advanced', request_method='GET',
486 486 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
487 487 def user_group_edit_advanced(self):
488 488 user_group = self.db_user_group
489 489
490 490 c = self.load_default_context()
491 491 c.user_group = user_group
492 492 c.active = 'advanced'
493 493 c.group_members_obj = sorted(
494 494 (x.user for x in c.user_group.members),
495 495 key=lambda u: u.username.lower())
496 496
497 497 c.group_to_repos = sorted(
498 498 (x.repository for x in c.user_group.users_group_repo_to_perm),
499 499 key=lambda u: u.repo_name.lower())
500 500
501 501 c.group_to_repo_groups = sorted(
502 502 (x.group for x in c.user_group.users_group_repo_group_to_perm),
503 503 key=lambda u: u.group_name.lower())
504 504
505 505 c.group_to_review_rules = sorted(
506 506 (x.users_group for x in c.user_group.user_group_review_rules),
507 507 key=lambda u: u.users_group_name.lower())
508 508
509 509 return self._get_template_context(c)
510 510
511 511 @LoginRequired()
512 512 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
513 513 @CSRFRequired()
514 514 @view_config(
515 515 route_name='edit_user_group_advanced_sync', request_method='POST',
516 516 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
517 517 def user_group_edit_advanced_set_synchronization(self):
518 518 _ = self.request.translate
519 519 user_group = self.db_user_group
520 520 user_group_id = user_group.users_group_id
521 521
522 522 existing = user_group.group_data.get('extern_type')
523 523
524 524 if existing:
525 525 new_state = user_group.group_data
526 526 new_state['extern_type'] = None
527 527 else:
528 528 new_state = user_group.group_data
529 529 new_state['extern_type'] = 'manual'
530 530 new_state['extern_type_set_by'] = self._rhodecode_user.username
531 531
532 532 try:
533 533 user_group.group_data = new_state
534 534 Session().add(user_group)
535 535 Session().commit()
536 536
537 537 h.flash(_('User Group synchronization updated successfully'),
538 538 category='success')
539 539 except Exception:
540 540 log.exception("Exception during sync settings saving")
541 541 h.flash(_('An error occurred during synchronization update'),
542 542 category='error')
543 543
544 544 raise HTTPFound(
545 545 h.route_path('edit_user_group_advanced',
546 546 user_group_id=user_group_id))
@@ -1,109 +1,106 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 import os
23 23 import logging
24 24 import rhodecode
25 25
26 26 # ------------------------------------------------------------------------------
27 27 # CELERY magic until refactor - issue #4163 - import order matters here:
28 28 #from rhodecode.lib import celerypylons # this must be first, celerypylons
29 29 # sets config settings upon import
30 30
31 31 import rhodecode.integrations # any modules using celery task
32 32 # decorators should be added afterwards:
33 33 # ------------------------------------------------------------------------------
34 34
35 35 from rhodecode.config import utils
36 36
37 37 from rhodecode.lib.utils import load_rcextensions
38 38 from rhodecode.lib.utils2 import str2bool
39 39 from rhodecode.lib.vcs import connect_vcs, start_vcs_server
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 def load_pyramid_environment(global_config, settings):
45 45 # Some parts of the code expect a merge of global and app settings.
46 46 settings_merged = global_config.copy()
47 47 settings_merged.update(settings)
48 48
49 49 # TODO(marcink): probably not required anymore
50 50 # configure channelstream,
51 51 settings_merged['channelstream_config'] = {
52 52 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
53 53 'server': settings_merged.get('channelstream.server'),
54 54 'secret': settings_merged.get('channelstream.secret')
55 55 }
56 56
57 57
58 58 # TODO(marcink): celery
59 59 # # store some globals into rhodecode
60 60 # rhodecode.CELERY_ENABLED = str2bool(config['app_conf'].get('use_celery'))
61 61 # rhodecode.CELERY_EAGER = str2bool(
62 62 # config['app_conf'].get('celery.always.eager'))
63 63
64 64
65 65 # If this is a test run we prepare the test environment like
66 66 # creating a test database, test search index and test repositories.
67 67 # This has to be done before the database connection is initialized.
68 68 if settings['is_test']:
69 69 rhodecode.is_test = True
70 70 rhodecode.disable_error_handler = True
71 71
72 72 utils.initialize_test_environment(settings_merged)
73 73
74 74 # Initialize the database connection.
75 75 utils.initialize_database(settings_merged)
76 76
77 # TODO(marcink): base_path handling ?
78 # repos_path = list(db_cfg.items('paths'))[0][1]
79
80 77 load_rcextensions(root_path=settings_merged['here'])
81 78
82 79 # Limit backends to `vcs.backends` from configuration
83 80 for alias in rhodecode.BACKENDS.keys():
84 81 if alias not in settings['vcs.backends']:
85 82 del rhodecode.BACKENDS[alias]
86 83 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
87 84
88 85 # initialize vcs client and optionally run the server if enabled
89 86 vcs_server_uri = settings['vcs.server']
90 87 vcs_server_enabled = settings['vcs.server.enable']
91 88 start_server = (
92 89 settings['vcs.start_server'] and
93 90 not int(os.environ.get('RC_VCSSERVER_TEST_DISABLE', '0')))
94 91
95 92 if vcs_server_enabled and start_server:
96 93 log.info("Starting vcsserver")
97 94 start_vcs_server(server_and_port=vcs_server_uri,
98 95 protocol=utils.get_vcs_server_protocol(settings),
99 96 log_level=settings['vcs.server.log_level'])
100 97
101 98 utils.configure_vcs(settings)
102 99
103 100 # Store the settings to make them available to other modules.
104 101
105 102 rhodecode.PYRAMID_SETTINGS = settings_merged
106 103 rhodecode.CONFIG = settings_merged
107 104
108 105 if vcs_server_enabled:
109 106 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
@@ -1,543 +1,540 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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 import pyramid.threadlocal
33 32
34 33 from paste.auth.basic import AuthBasicAuthenticator
35 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37 36
38 37 import rhodecode
39 38 from rhodecode.authentication.base import VCS_TYPE
40 39 from rhodecode.lib import auth, utils2
41 40 from rhodecode.lib import helpers as h
42 41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 42 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
47 44 from rhodecode.lib.utils2 import (
48 45 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 from rhodecode.model import meta
50 46 from rhodecode.model.db import Repository, User, ChangesetComment
51 47 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
53 48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54 49
55 50 log = logging.getLogger(__name__)
56 51
57 52
58 53 def _filter_proxy(ip):
59 54 """
60 55 Passed in IP addresses in HEADERS can be in a special format of multiple
61 56 ips. Those comma separated IPs are passed from various proxies in the
62 57 chain of request processing. The left-most being the original client.
63 58 We only care about the first IP which came from the org. client.
64 59
65 60 :param ip: ip string from headers
66 61 """
67 62 if ',' in ip:
68 63 _ips = ip.split(',')
69 64 _first_ip = _ips[0].strip()
70 65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
71 66 return _first_ip
72 67 return ip
73 68
74 69
75 70 def _filter_port(ip):
76 71 """
77 72 Removes a port from ip, there are 4 main cases to handle here.
78 73 - ipv4 eg. 127.0.0.1
79 74 - ipv6 eg. ::1
80 75 - ipv4+port eg. 127.0.0.1:8080
81 76 - ipv6+port eg. [::1]:8080
82 77
83 78 :param ip:
84 79 """
85 80 def is_ipv6(ip_addr):
86 81 if hasattr(socket, 'inet_pton'):
87 82 try:
88 83 socket.inet_pton(socket.AF_INET6, ip_addr)
89 84 except socket.error:
90 85 return False
91 86 else:
92 87 # fallback to ipaddress
93 88 try:
94 89 ipaddress.IPv6Address(safe_unicode(ip_addr))
95 90 except Exception:
96 91 return False
97 92 return True
98 93
99 94 if ':' not in ip: # must be ipv4 pure ip
100 95 return ip
101 96
102 97 if '[' in ip and ']' in ip: # ipv6 with port
103 98 return ip.split(']')[0][1:].lower()
104 99
105 100 # must be ipv6 or ipv4 with port
106 101 if is_ipv6(ip):
107 102 return ip
108 103 else:
109 104 ip, _port = ip.split(':')[:2] # means ipv4+port
110 105 return ip
111 106
112 107
113 108 def get_ip_addr(environ):
114 109 proxy_key = 'HTTP_X_REAL_IP'
115 110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
116 111 def_key = 'REMOTE_ADDR'
117 112 _filters = lambda x: _filter_port(_filter_proxy(x))
118 113
119 114 ip = environ.get(proxy_key)
120 115 if ip:
121 116 return _filters(ip)
122 117
123 118 ip = environ.get(proxy_key2)
124 119 if ip:
125 120 return _filters(ip)
126 121
127 122 ip = environ.get(def_key, '0.0.0.0')
128 123 return _filters(ip)
129 124
130 125
131 126 def get_server_ip_addr(environ, log_errors=True):
132 127 hostname = environ.get('SERVER_NAME')
133 128 try:
134 129 return socket.gethostbyname(hostname)
135 130 except Exception as e:
136 131 if log_errors:
137 132 # in some cases this lookup is not possible, and we don't want to
138 133 # make it an exception in logs
139 134 log.exception('Could not retrieve server ip address: %s', e)
140 135 return hostname
141 136
142 137
143 138 def get_server_port(environ):
144 139 return environ.get('SERVER_PORT')
145 140
146 141
147 142 def get_access_path(environ):
148 143 path = environ.get('PATH_INFO')
149 144 org_req = environ.get('pylons.original_request')
150 145 if org_req:
151 146 path = org_req.environ.get('PATH_INFO')
152 147 return path
153 148
154 149
155 150 def get_user_agent(environ):
156 151 return environ.get('HTTP_USER_AGENT')
157 152
158 153
159 154 def vcs_operation_context(
160 155 environ, repo_name, username, action, scm, check_locking=True,
161 156 is_shadow_repo=False):
162 157 """
163 158 Generate the context for a vcs operation, e.g. push or pull.
164 159
165 160 This context is passed over the layers so that hooks triggered by the
166 161 vcs operation know details like the user, the user's IP address etc.
167 162
168 163 :param check_locking: Allows to switch of the computation of the locking
169 164 data. This serves mainly the need of the simplevcs middleware to be
170 165 able to disable this for certain operations.
171 166
172 167 """
173 168 # Tri-state value: False: unlock, None: nothing, True: lock
174 169 make_lock = None
175 170 locked_by = [None, None, None]
176 171 is_anonymous = username == User.DEFAULT_USER
177 172 if not is_anonymous and check_locking:
178 173 log.debug('Checking locking on repository "%s"', repo_name)
179 174 user = User.get_by_username(username)
180 175 repo = Repository.get_by_repo_name(repo_name)
181 176 make_lock, __, locked_by = repo.get_locking_state(
182 177 action, user.user_id)
183 178
184 179 settings_model = VcsSettingsModel(repo=repo_name)
185 180 ui_settings = settings_model.get_ui_settings()
186 181
187 182 extras = {
188 183 'ip': get_ip_addr(environ),
189 184 'username': username,
190 185 'action': action,
191 186 'repository': repo_name,
192 187 'scm': scm,
193 188 'config': rhodecode.CONFIG['__file__'],
194 189 'make_lock': make_lock,
195 190 'locked_by': locked_by,
196 191 'server_url': utils2.get_server_url(environ),
197 192 'user_agent': get_user_agent(environ),
198 193 'hooks': get_enabled_hook_classes(ui_settings),
199 194 'is_shadow_repo': is_shadow_repo,
200 195 }
201 196 return extras
202 197
203 198
204 199 class BasicAuth(AuthBasicAuthenticator):
205 200
206 201 def __init__(self, realm, authfunc, registry, auth_http_code=None,
207 202 initial_call_detection=False, acl_repo_name=None):
208 203 self.realm = realm
209 204 self.initial_call = initial_call_detection
210 205 self.authfunc = authfunc
211 206 self.registry = registry
212 207 self.acl_repo_name = acl_repo_name
213 208 self._rc_auth_http_code = auth_http_code
214 209
215 210 def _get_response_from_code(self, http_code):
216 211 try:
217 212 return get_exception(safe_int(http_code))
218 213 except Exception:
219 214 log.exception('Failed to fetch response for code %s' % http_code)
220 215 return HTTPForbidden
221 216
222 217 def get_rc_realm(self):
223 218 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
224 219
225 220 def build_authentication(self):
226 221 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
227 222 if self._rc_auth_http_code and not self.initial_call:
228 223 # return alternative HTTP code if alternative http return code
229 224 # is specified in RhodeCode config, but ONLY if it's not the
230 225 # FIRST call
231 226 custom_response_klass = self._get_response_from_code(
232 227 self._rc_auth_http_code)
233 228 return custom_response_klass(headers=head)
234 229 return HTTPUnauthorized(headers=head)
235 230
236 231 def authenticate(self, environ):
237 232 authorization = AUTHORIZATION(environ)
238 233 if not authorization:
239 234 return self.build_authentication()
240 235 (authmeth, auth) = authorization.split(' ', 1)
241 236 if 'basic' != authmeth.lower():
242 237 return self.build_authentication()
243 238 auth = auth.strip().decode('base64')
244 239 _parts = auth.split(':', 1)
245 240 if len(_parts) == 2:
246 241 username, password = _parts
247 242 auth_data = self.authfunc(
248 243 username, password, environ, VCS_TYPE,
249 244 registry=self.registry, acl_repo_name=self.acl_repo_name)
250 245 if auth_data:
251 246 return {'username': username, 'auth_data': auth_data}
252 247 if username and password:
253 248 # we mark that we actually executed authentication once, at
254 249 # that point we can use the alternative auth code
255 250 self.initial_call = False
256 251
257 252 return self.build_authentication()
258 253
259 254 __call__ = authenticate
260 255
261 256
262 257 def calculate_version_hash(config):
263 258 return md5(
264 259 config.get('beaker.session.secret', '') +
265 260 rhodecode.__version__)[:8]
266 261
267 262
268 263 def get_current_lang(request):
269 264 # NOTE(marcink): remove after pyramid move
270 265 try:
271 266 return translation.get_lang()[0]
272 267 except:
273 268 pass
274 269
275 270 return getattr(request, '_LOCALE_', request.locale_name)
276 271
277 272
278 273 def attach_context_attributes(context, request, user_id):
279 274 """
280 275 Attach variables into template context called `c`.
281 276 """
282 277 config = request.registry.settings
283 278
284 279
285 280 rc_config = SettingsModel().get_all_settings(cache=True)
286 281
287 282 context.rhodecode_version = rhodecode.__version__
288 283 context.rhodecode_edition = config.get('rhodecode.edition')
289 284 # unique secret + version does not leak the version but keep consistency
290 285 context.rhodecode_version_hash = calculate_version_hash(config)
291 286
292 287 # Default language set for the incoming request
293 288 context.language = get_current_lang(request)
294 289
295 290 # Visual options
296 291 context.visual = AttributeDict({})
297 292
298 293 # DB stored Visual Items
299 294 context.visual.show_public_icon = str2bool(
300 295 rc_config.get('rhodecode_show_public_icon'))
301 296 context.visual.show_private_icon = str2bool(
302 297 rc_config.get('rhodecode_show_private_icon'))
303 298 context.visual.stylify_metatags = str2bool(
304 299 rc_config.get('rhodecode_stylify_metatags'))
305 300 context.visual.dashboard_items = safe_int(
306 301 rc_config.get('rhodecode_dashboard_items', 100))
307 302 context.visual.admin_grid_items = safe_int(
308 303 rc_config.get('rhodecode_admin_grid_items', 100))
309 304 context.visual.repository_fields = str2bool(
310 305 rc_config.get('rhodecode_repository_fields'))
311 306 context.visual.show_version = str2bool(
312 307 rc_config.get('rhodecode_show_version'))
313 308 context.visual.use_gravatar = str2bool(
314 309 rc_config.get('rhodecode_use_gravatar'))
315 310 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
316 311 context.visual.default_renderer = rc_config.get(
317 312 'rhodecode_markup_renderer', 'rst')
318 313 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
319 314 context.visual.rhodecode_support_url = \
320 315 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
321 316
322 317 context.visual.affected_files_cut_off = 60
323 318
324 319 context.pre_code = rc_config.get('rhodecode_pre_code')
325 320 context.post_code = rc_config.get('rhodecode_post_code')
326 321 context.rhodecode_name = rc_config.get('rhodecode_title')
327 322 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
328 323 # if we have specified default_encoding in the request, it has more
329 324 # priority
330 325 if request.GET.get('default_encoding'):
331 326 context.default_encodings.insert(0, request.GET.get('default_encoding'))
332 327 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
333 328
334 329 # INI stored
335 330 context.labs_active = str2bool(
336 331 config.get('labs_settings_active', 'false'))
337 332 context.visual.allow_repo_location_change = str2bool(
338 333 config.get('allow_repo_location_change', True))
339 334 context.visual.allow_custom_hooks_settings = str2bool(
340 335 config.get('allow_custom_hooks_settings', True))
341 336 context.debug_style = str2bool(config.get('debug_style', False))
342 337
343 338 context.rhodecode_instanceid = config.get('instance_id')
344 339
345 340 context.visual.cut_off_limit_diff = safe_int(
346 341 config.get('cut_off_limit_diff'))
347 342 context.visual.cut_off_limit_file = safe_int(
348 343 config.get('cut_off_limit_file'))
349 344
350 345 # AppEnlight
351 346 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
352 347 context.appenlight_api_public_key = config.get(
353 348 'appenlight.api_public_key', '')
354 349 context.appenlight_server_url = config.get('appenlight.server_url', '')
355 350
356 351 # JS template context
357 352 context.template_context = {
358 353 'repo_name': None,
359 354 'repo_type': None,
360 355 'repo_landing_commit': None,
361 356 'rhodecode_user': {
362 357 'username': None,
363 358 'email': None,
364 359 'notification_status': False
365 360 },
366 361 'visual': {
367 362 'default_renderer': None
368 363 },
369 364 'commit_data': {
370 365 'commit_id': None
371 366 },
372 367 'pull_request_data': {'pull_request_id': None},
373 368 'timeago': {
374 369 'refresh_time': 120 * 1000,
375 370 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
376 371 },
377 372 'pyramid_dispatch': {
378 373
379 374 },
380 375 'extra': {'plugins': {}}
381 376 }
382 377 # END CONFIG VARS
383 378
384 379 diffmode = 'sideside'
385 380 if request.GET.get('diffmode'):
386 381 if request.GET['diffmode'] == 'unified':
387 382 diffmode = 'unified'
388 383 elif request.session.get('diffmode'):
389 384 diffmode = request.session['diffmode']
390 385
391 386 context.diffmode = diffmode
392 387
393 388 if request.session.get('diffmode') != diffmode:
394 389 request.session['diffmode'] = diffmode
395 390
396 391 context.csrf_token = auth.get_csrf_token(session=request.session)
397 392 context.backends = rhodecode.BACKENDS.keys()
398 393 context.backends.sort()
399 394 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400 395
401 396 # web case
402 397 if hasattr(request, 'user'):
403 398 context.auth_user = request.user
404 399 context.rhodecode_user = request.user
405 400
406 401 # api case
407 402 if hasattr(request, 'rpc_user'):
408 403 context.auth_user = request.rpc_user
409 404 context.rhodecode_user = request.rpc_user
410 405
411 406 # attach the whole call context to the request
412 407 request.call_context = context
413 408
414 409
415 410 def get_auth_user(request):
416 411 environ = request.environ
417 412 session = request.session
418 413
419 414 ip_addr = get_ip_addr(environ)
420 415 # make sure that we update permissions each time we call controller
421 416 _auth_token = (request.GET.get('auth_token', '') or
422 417 request.GET.get('api_key', ''))
423 418
424 419 if _auth_token:
425 420 # when using API_KEY we assume user exists, and
426 421 # doesn't need auth based on cookies.
427 422 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
428 423 authenticated = False
429 424 else:
430 425 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
431 426 try:
432 427 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
433 428 ip_addr=ip_addr)
434 429 except UserCreationError as e:
435 430 h.flash(e, 'error')
436 431 # container auth or other auth functions that create users
437 432 # on the fly can throw this exception signaling that there's
438 433 # issue with user creation, explanation should be provided
439 434 # in Exception itself. We then create a simple blank
440 435 # AuthUser
441 436 auth_user = AuthUser(ip_addr=ip_addr)
442 437
438 # in case someone changes a password for user it triggers session
439 # flush and forces a re-login
443 440 if password_changed(auth_user, session):
444 441 session.invalidate()
445 442 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
446 443 auth_user = AuthUser(ip_addr=ip_addr)
447 444
448 445 authenticated = cookie_store.get('is_authenticated')
449 446
450 447 if not auth_user.is_authenticated and auth_user.is_user_object:
451 448 # user is not authenticated and not empty
452 449 auth_user.set_authenticated(authenticated)
453 450
454 451 return auth_user
455 452
456 453
457 454 def h_filter(s):
458 455 """
459 456 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
460 457 we wrap this with additional functionality that converts None to empty
461 458 strings
462 459 """
463 460 if s is None:
464 461 return markupsafe.Markup()
465 462 return markupsafe.escape(s)
466 463
467 464
468 465 def add_events_routes(config):
469 466 """
470 467 Adds routing that can be used in events. Because some events are triggered
471 468 outside of pyramid context, we need to bootstrap request with some
472 469 routing registered
473 470 """
474 471
475 472 from rhodecode.apps._base import ADMIN_PREFIX
476 473
477 474 config.add_route(name='home', pattern='/')
478 475
479 476 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
480 477 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
481 478 config.add_route(name='repo_summary', pattern='/{repo_name}')
482 479 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
483 480 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
484 481
485 482 config.add_route(name='pullrequest_show',
486 483 pattern='/{repo_name}/pull-request/{pull_request_id}')
487 484 config.add_route(name='pull_requests_global',
488 485 pattern='/pull-request/{pull_request_id}')
489 486 config.add_route(name='repo_commit',
490 487 pattern='/{repo_name}/changeset/{commit_id}')
491 488
492 489 config.add_route(name='repo_files',
493 490 pattern='/{repo_name}/files/{commit_id}/{f_path}')
494 491
495 492
496 493 def bootstrap_config(request):
497 494 import pyramid.testing
498 495 registry = pyramid.testing.Registry('RcTestRegistry')
499 496
500 497 config = pyramid.testing.setUp(registry=registry, request=request)
501 498
502 499 # allow pyramid lookup in testing
503 500 config.include('pyramid_mako')
504 501 config.include('pyramid_beaker')
505 502
506 503 add_events_routes(config)
507 504
508 505 return config
509 506
510 507
511 508 def bootstrap_request(**kwargs):
512 509 import pyramid.testing
513 510
514 511 class TestRequest(pyramid.testing.DummyRequest):
515 512 application_url = kwargs.pop('application_url', 'http://example.com')
516 513 host = kwargs.pop('host', 'example.com:80')
517 514 domain = kwargs.pop('domain', 'example.com')
518 515
519 516 def translate(self, msg):
520 517 return msg
521 518
522 519 def plularize(self, singular, plural, n):
523 520 return singular
524 521
525 522 def get_partial_renderer(self, tmpl_name):
526 523
527 524 from rhodecode.lib.partial_renderer import get_partial_renderer
528 525 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
529 526
530 527 _call_context = {}
531 528 @property
532 529 def call_context(self):
533 530 return self._call_context
534 531
535 532 class TestDummySession(pyramid.testing.DummySession):
536 533 def save(*arg, **kw):
537 534 pass
538 535
539 536 request = TestRequest(**kwargs)
540 537 request.session = TestDummySession()
541 538
542 539 return request
543 540
@@ -1,1170 +1,1107 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of diffing helpers, previously part of vcs
24 24 """
25 25
26 import re
26 27 import collections
27 import re
28 28 import difflib
29 29 import logging
30 30
31 31 from itertools import tee, imap
32 32
33 from rhodecode.translation import temp_translation_factory as _
34
35 33 from rhodecode.lib.vcs.exceptions import VCSError
36 34 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 from rhodecode.lib.helpers import escape
39 35 from rhodecode.lib.utils2 import safe_unicode
40 36
41 37 log = logging.getLogger(__name__)
42 38
43 39 # define max context, a file with more than this numbers of lines is unusable
44 40 # in browser anyway
45 41 MAX_CONTEXT = 1024 * 1014
46 42
47 43
48 44 class OPS(object):
49 45 ADD = 'A'
50 46 MOD = 'M'
51 47 DEL = 'D'
52 48
53 49
54 def wrap_to_table(str_):
55 return '''<table class="code-difftable">
56 <tr class="line no-comment">
57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
58 <td></td>
59 <td class="lineno new"></td>
60 <td class="code no-comment"><pre>%s</pre></td>
61 </tr>
62 </table>''' % (_('Click to comment'), str_)
63
64
65 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
66 show_full_diff=False, ignore_whitespace=True, line_context=3,
67 enable_comments=False):
68 """
69 returns a wrapped diff into a table, checks for cut_off_limit for file and
70 whole diff and presents proper message
71 """
72
73 if filenode_old is None:
74 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
75
76 if filenode_old.is_binary or filenode_new.is_binary:
77 diff = wrap_to_table(_('Binary file'))
78 stats = None
79 size = 0
80 data = None
81
82 elif diff_limit != -1 and (diff_limit is None or
83 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
84
85 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
86 ignore_whitespace=ignore_whitespace,
87 context=line_context)
88 diff_processor = DiffProcessor(
89 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
90 file_limit=file_limit, show_full_diff=show_full_diff)
91 _parsed = diff_processor.prepare()
92
93 diff = diff_processor.as_html(enable_comments=enable_comments)
94 stats = _parsed[0]['stats'] if _parsed else None
95 size = len(diff or '')
96 data = _parsed[0] if _parsed else None
97 else:
98 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
99 'diff menu to display this diff'))
100 stats = None
101 size = 0
102 data = None
103 if not diff:
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
106 if submodules:
107 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
108 else:
109 diff = wrap_to_table(_('No changes detected'))
110
111 cs1 = filenode_old.commit.raw_id
112 cs2 = filenode_new.commit.raw_id
113
114 return size, cs1, cs2, diff, stats, data
115
116
117 50 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
118 51 """
119 52 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
120 53
121 54 :param ignore_whitespace: ignore whitespaces in diff
122 55 """
123 56 # make sure we pass in default context
124 57 context = context or 3
125 58 # protect against IntOverflow when passing HUGE context
126 59 if context > MAX_CONTEXT:
127 60 context = MAX_CONTEXT
128 61
129 62 submodules = filter(lambda o: isinstance(o, SubModuleNode),
130 63 [filenode_new, filenode_old])
131 64 if submodules:
132 65 return ''
133 66
134 67 for filenode in (filenode_old, filenode_new):
135 68 if not isinstance(filenode, FileNode):
136 69 raise VCSError(
137 70 "Given object should be FileNode object, not %s"
138 71 % filenode.__class__)
139 72
140 73 repo = filenode_new.commit.repository
141 74 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
142 75 new_commit = filenode_new.commit
143 76
144 77 vcs_gitdiff = repo.get_diff(
145 78 old_commit, new_commit, filenode_new.path,
146 79 ignore_whitespace, context, path1=filenode_old.path)
147 80 return vcs_gitdiff
148 81
149 82 NEW_FILENODE = 1
150 83 DEL_FILENODE = 2
151 84 MOD_FILENODE = 3
152 85 RENAMED_FILENODE = 4
153 86 COPIED_FILENODE = 5
154 87 CHMOD_FILENODE = 6
155 88 BIN_FILENODE = 7
156 89
157 90
158 91 class LimitedDiffContainer(object):
159 92
160 93 def __init__(self, diff_limit, cur_diff_size, diff):
161 94 self.diff = diff
162 95 self.diff_limit = diff_limit
163 96 self.cur_diff_size = cur_diff_size
164 97
165 98 def __getitem__(self, key):
166 99 return self.diff.__getitem__(key)
167 100
168 101 def __iter__(self):
169 102 for l in self.diff:
170 103 yield l
171 104
172 105
173 106 class Action(object):
174 107 """
175 108 Contains constants for the action value of the lines in a parsed diff.
176 109 """
177 110
178 111 ADD = 'add'
179 112 DELETE = 'del'
180 113 UNMODIFIED = 'unmod'
181 114
182 115 CONTEXT = 'context'
183 116 OLD_NO_NL = 'old-no-nl'
184 117 NEW_NO_NL = 'new-no-nl'
185 118
186 119
187 120 class DiffProcessor(object):
188 121 """
189 122 Give it a unified or git diff and it returns a list of the files that were
190 123 mentioned in the diff together with a dict of meta information that
191 124 can be used to render it in a HTML template.
192 125
193 126 .. note:: Unicode handling
194 127
195 128 The original diffs are a byte sequence and can contain filenames
196 129 in mixed encodings. This class generally returns `unicode` objects
197 130 since the result is intended for presentation to the user.
198 131
199 132 """
200 133 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
201 134 _newline_marker = re.compile(r'^\\ No newline at end of file')
202 135
203 136 # used for inline highlighter word split
204 137 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
205 138
206 139 # collapse ranges of commits over given number
207 140 _collapse_commits_over = 5
208 141
209 142 def __init__(self, diff, format='gitdiff', diff_limit=None,
210 143 file_limit=None, show_full_diff=True):
211 144 """
212 145 :param diff: A `Diff` object representing a diff from a vcs backend
213 146 :param format: format of diff passed, `udiff` or `gitdiff`
214 147 :param diff_limit: define the size of diff that is considered "big"
215 148 based on that parameter cut off will be triggered, set to None
216 149 to show full diff
217 150 """
218 151 self._diff = diff
219 152 self._format = format
220 153 self.adds = 0
221 154 self.removes = 0
222 155 # calculate diff size
223 156 self.diff_limit = diff_limit
224 157 self.file_limit = file_limit
225 158 self.show_full_diff = show_full_diff
226 159 self.cur_diff_size = 0
227 160 self.parsed = False
228 161 self.parsed_diff = []
229 162
230 163 log.debug('Initialized DiffProcessor with %s mode', format)
231 164 if format == 'gitdiff':
232 165 self.differ = self._highlight_line_difflib
233 166 self._parser = self._parse_gitdiff
234 167 else:
235 168 self.differ = self._highlight_line_udiff
236 169 self._parser = self._new_parse_gitdiff
237 170
238 171 def _copy_iterator(self):
239 172 """
240 173 make a fresh copy of generator, we should not iterate thru
241 174 an original as it's needed for repeating operations on
242 175 this instance of DiffProcessor
243 176 """
244 177 self.__udiff, iterator_copy = tee(self.__udiff)
245 178 return iterator_copy
246 179
247 180 def _escaper(self, string):
248 181 """
249 182 Escaper for diff escapes special chars and checks the diff limit
250 183
251 184 :param string:
252 185 """
253 186
254 187 self.cur_diff_size += len(string)
255 188
256 189 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
257 190 raise DiffLimitExceeded('Diff Limit Exceeded')
258 191
259 192 return safe_unicode(string)\
260 193 .replace('&', '&amp;')\
261 194 .replace('<', '&lt;')\
262 195 .replace('>', '&gt;')
263 196
264 197 def _line_counter(self, l):
265 198 """
266 199 Checks each line and bumps total adds/removes for this diff
267 200
268 201 :param l:
269 202 """
270 203 if l.startswith('+') and not l.startswith('+++'):
271 204 self.adds += 1
272 205 elif l.startswith('-') and not l.startswith('---'):
273 206 self.removes += 1
274 207 return safe_unicode(l)
275 208
276 209 def _highlight_line_difflib(self, line, next_):
277 210 """
278 211 Highlight inline changes in both lines.
279 212 """
280 213
281 214 if line['action'] == Action.DELETE:
282 215 old, new = line, next_
283 216 else:
284 217 old, new = next_, line
285 218
286 219 oldwords = self._token_re.split(old['line'])
287 220 newwords = self._token_re.split(new['line'])
288 221 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
289 222
290 223 oldfragments, newfragments = [], []
291 224 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
292 225 oldfrag = ''.join(oldwords[i1:i2])
293 226 newfrag = ''.join(newwords[j1:j2])
294 227 if tag != 'equal':
295 228 if oldfrag:
296 229 oldfrag = '<del>%s</del>' % oldfrag
297 230 if newfrag:
298 231 newfrag = '<ins>%s</ins>' % newfrag
299 232 oldfragments.append(oldfrag)
300 233 newfragments.append(newfrag)
301 234
302 235 old['line'] = "".join(oldfragments)
303 236 new['line'] = "".join(newfragments)
304 237
305 238 def _highlight_line_udiff(self, line, next_):
306 239 """
307 240 Highlight inline changes in both lines.
308 241 """
309 242 start = 0
310 243 limit = min(len(line['line']), len(next_['line']))
311 244 while start < limit and line['line'][start] == next_['line'][start]:
312 245 start += 1
313 246 end = -1
314 247 limit -= start
315 248 while -end <= limit and line['line'][end] == next_['line'][end]:
316 249 end -= 1
317 250 end += 1
318 251 if start or end:
319 252 def do(l):
320 253 last = end + len(l['line'])
321 254 if l['action'] == Action.ADD:
322 255 tag = 'ins'
323 256 else:
324 257 tag = 'del'
325 258 l['line'] = '%s<%s>%s</%s>%s' % (
326 259 l['line'][:start],
327 260 tag,
328 261 l['line'][start:last],
329 262 tag,
330 263 l['line'][last:]
331 264 )
332 265 do(line)
333 266 do(next_)
334 267
335 268 def _clean_line(self, line, command):
336 269 if command in ['+', '-', ' ']:
337 270 # only modify the line if it's actually a diff thing
338 271 line = line[1:]
339 272 return line
340 273
341 274 def _parse_gitdiff(self, inline_diff=True):
342 275 _files = []
343 276 diff_container = lambda arg: arg
344 277
345 278 for chunk in self._diff.chunks():
346 279 head = chunk.header
347 280
348 281 diff = imap(self._escaper, chunk.diff.splitlines(1))
349 282 raw_diff = chunk.raw
350 283 limited_diff = False
351 284 exceeds_limit = False
352 285
353 286 op = None
354 287 stats = {
355 288 'added': 0,
356 289 'deleted': 0,
357 290 'binary': False,
358 291 'ops': {},
359 292 }
360 293
361 294 if head['deleted_file_mode']:
362 295 op = OPS.DEL
363 296 stats['binary'] = True
364 297 stats['ops'][DEL_FILENODE] = 'deleted file'
365 298
366 299 elif head['new_file_mode']:
367 300 op = OPS.ADD
368 301 stats['binary'] = True
369 302 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
370 303 else: # modify operation, can be copy, rename or chmod
371 304
372 305 # CHMOD
373 306 if head['new_mode'] and head['old_mode']:
374 307 op = OPS.MOD
375 308 stats['binary'] = True
376 309 stats['ops'][CHMOD_FILENODE] = (
377 310 'modified file chmod %s => %s' % (
378 311 head['old_mode'], head['new_mode']))
379 312 # RENAME
380 313 if head['rename_from'] != head['rename_to']:
381 314 op = OPS.MOD
382 315 stats['binary'] = True
383 316 stats['ops'][RENAMED_FILENODE] = (
384 317 'file renamed from %s to %s' % (
385 318 head['rename_from'], head['rename_to']))
386 319 # COPY
387 320 if head.get('copy_from') and head.get('copy_to'):
388 321 op = OPS.MOD
389 322 stats['binary'] = True
390 323 stats['ops'][COPIED_FILENODE] = (
391 324 'file copied from %s to %s' % (
392 325 head['copy_from'], head['copy_to']))
393 326
394 327 # If our new parsed headers didn't match anything fallback to
395 328 # old style detection
396 329 if op is None:
397 330 if not head['a_file'] and head['b_file']:
398 331 op = OPS.ADD
399 332 stats['binary'] = True
400 333 stats['ops'][NEW_FILENODE] = 'new file'
401 334
402 335 elif head['a_file'] and not head['b_file']:
403 336 op = OPS.DEL
404 337 stats['binary'] = True
405 338 stats['ops'][DEL_FILENODE] = 'deleted file'
406 339
407 340 # it's not ADD not DELETE
408 341 if op is None:
409 342 op = OPS.MOD
410 343 stats['binary'] = True
411 344 stats['ops'][MOD_FILENODE] = 'modified file'
412 345
413 346 # a real non-binary diff
414 347 if head['a_file'] or head['b_file']:
415 348 try:
416 349 raw_diff, chunks, _stats = self._parse_lines(diff)
417 350 stats['binary'] = False
418 351 stats['added'] = _stats[0]
419 352 stats['deleted'] = _stats[1]
420 353 # explicit mark that it's a modified file
421 354 if op == OPS.MOD:
422 355 stats['ops'][MOD_FILENODE] = 'modified file'
423 356 exceeds_limit = len(raw_diff) > self.file_limit
424 357
425 358 # changed from _escaper function so we validate size of
426 359 # each file instead of the whole diff
427 360 # diff will hide big files but still show small ones
428 361 # from my tests, big files are fairly safe to be parsed
429 362 # but the browser is the bottleneck
430 363 if not self.show_full_diff and exceeds_limit:
431 364 raise DiffLimitExceeded('File Limit Exceeded')
432 365
433 366 except DiffLimitExceeded:
434 367 diff_container = lambda _diff: \
435 368 LimitedDiffContainer(
436 369 self.diff_limit, self.cur_diff_size, _diff)
437 370
438 371 exceeds_limit = len(raw_diff) > self.file_limit
439 372 limited_diff = True
440 373 chunks = []
441 374
442 375 else: # GIT format binary patch, or possibly empty diff
443 376 if head['bin_patch']:
444 377 # we have operation already extracted, but we mark simply
445 378 # it's a diff we wont show for binary files
446 379 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
447 380 chunks = []
448 381
449 382 if chunks and not self.show_full_diff and op == OPS.DEL:
450 383 # if not full diff mode show deleted file contents
451 384 # TODO: anderson: if the view is not too big, there is no way
452 385 # to see the content of the file
453 386 chunks = []
454 387
455 388 chunks.insert(0, [{
456 389 'old_lineno': '',
457 390 'new_lineno': '',
458 391 'action': Action.CONTEXT,
459 392 'line': msg,
460 393 } for _op, msg in stats['ops'].iteritems()
461 394 if _op not in [MOD_FILENODE]])
462 395
463 396 _files.append({
464 397 'filename': safe_unicode(head['b_path']),
465 398 'old_revision': head['a_blob_id'],
466 399 'new_revision': head['b_blob_id'],
467 400 'chunks': chunks,
468 401 'raw_diff': safe_unicode(raw_diff),
469 402 'operation': op,
470 403 'stats': stats,
471 404 'exceeds_limit': exceeds_limit,
472 405 'is_limited_diff': limited_diff,
473 406 })
474 407
475 408 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
476 409 OPS.DEL: 2}.get(info['operation'])
477 410
478 411 if not inline_diff:
479 412 return diff_container(sorted(_files, key=sorter))
480 413
481 414 # highlight inline changes
482 415 for diff_data in _files:
483 416 for chunk in diff_data['chunks']:
484 417 lineiter = iter(chunk)
485 418 try:
486 419 while 1:
487 420 line = lineiter.next()
488 421 if line['action'] not in (
489 422 Action.UNMODIFIED, Action.CONTEXT):
490 423 nextline = lineiter.next()
491 424 if nextline['action'] in ['unmod', 'context'] or \
492 425 nextline['action'] == line['action']:
493 426 continue
494 427 self.differ(line, nextline)
495 428 except StopIteration:
496 429 pass
497 430
498 431 return diff_container(sorted(_files, key=sorter))
499 432
500 433 def _check_large_diff(self):
501 434 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
502 435 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
503 436 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
504 437
505 438 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
506 439 def _new_parse_gitdiff(self, inline_diff=True):
507 440 _files = []
508 441
509 442 # this can be overriden later to a LimitedDiffContainer type
510 443 diff_container = lambda arg: arg
511 444
512 445 for chunk in self._diff.chunks():
513 446 head = chunk.header
514 447 log.debug('parsing diff %r' % head)
515 448
516 449 raw_diff = chunk.raw
517 450 limited_diff = False
518 451 exceeds_limit = False
519 452
520 453 op = None
521 454 stats = {
522 455 'added': 0,
523 456 'deleted': 0,
524 457 'binary': False,
525 458 'old_mode': None,
526 459 'new_mode': None,
527 460 'ops': {},
528 461 }
529 462 if head['old_mode']:
530 463 stats['old_mode'] = head['old_mode']
531 464 if head['new_mode']:
532 465 stats['new_mode'] = head['new_mode']
533 466 if head['b_mode']:
534 467 stats['new_mode'] = head['b_mode']
535 468
536 469 # delete file
537 470 if head['deleted_file_mode']:
538 471 op = OPS.DEL
539 472 stats['binary'] = True
540 473 stats['ops'][DEL_FILENODE] = 'deleted file'
541 474
542 475 # new file
543 476 elif head['new_file_mode']:
544 477 op = OPS.ADD
545 478 stats['binary'] = True
546 479 stats['old_mode'] = None
547 480 stats['new_mode'] = head['new_file_mode']
548 481 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
549 482
550 483 # modify operation, can be copy, rename or chmod
551 484 else:
552 485 # CHMOD
553 486 if head['new_mode'] and head['old_mode']:
554 487 op = OPS.MOD
555 488 stats['binary'] = True
556 489 stats['ops'][CHMOD_FILENODE] = (
557 490 'modified file chmod %s => %s' % (
558 491 head['old_mode'], head['new_mode']))
559 492
560 493 # RENAME
561 494 if head['rename_from'] != head['rename_to']:
562 495 op = OPS.MOD
563 496 stats['binary'] = True
564 497 stats['renamed'] = (head['rename_from'], head['rename_to'])
565 498 stats['ops'][RENAMED_FILENODE] = (
566 499 'file renamed from %s to %s' % (
567 500 head['rename_from'], head['rename_to']))
568 501 # COPY
569 502 if head.get('copy_from') and head.get('copy_to'):
570 503 op = OPS.MOD
571 504 stats['binary'] = True
572 505 stats['copied'] = (head['copy_from'], head['copy_to'])
573 506 stats['ops'][COPIED_FILENODE] = (
574 507 'file copied from %s to %s' % (
575 508 head['copy_from'], head['copy_to']))
576 509
577 510 # If our new parsed headers didn't match anything fallback to
578 511 # old style detection
579 512 if op is None:
580 513 if not head['a_file'] and head['b_file']:
581 514 op = OPS.ADD
582 515 stats['binary'] = True
583 516 stats['new_file'] = True
584 517 stats['ops'][NEW_FILENODE] = 'new file'
585 518
586 519 elif head['a_file'] and not head['b_file']:
587 520 op = OPS.DEL
588 521 stats['binary'] = True
589 522 stats['ops'][DEL_FILENODE] = 'deleted file'
590 523
591 524 # it's not ADD not DELETE
592 525 if op is None:
593 526 op = OPS.MOD
594 527 stats['binary'] = True
595 528 stats['ops'][MOD_FILENODE] = 'modified file'
596 529
597 530 # a real non-binary diff
598 531 if head['a_file'] or head['b_file']:
599 532 diff = iter(chunk.diff.splitlines(1))
600 533
601 534 # append each file to the diff size
602 535 raw_chunk_size = len(raw_diff)
603 536
604 537 exceeds_limit = raw_chunk_size > self.file_limit
605 538 self.cur_diff_size += raw_chunk_size
606 539
607 540 try:
608 541 # Check each file instead of the whole diff.
609 542 # Diff will hide big files but still show small ones.
610 543 # From the tests big files are fairly safe to be parsed
611 544 # but the browser is the bottleneck.
612 545 if not self.show_full_diff and exceeds_limit:
613 546 log.debug('File `%s` exceeds current file_limit of %s',
614 547 safe_unicode(head['b_path']), self.file_limit)
615 548 raise DiffLimitExceeded(
616 549 'File Limit %s Exceeded', self.file_limit)
617 550
618 551 self._check_large_diff()
619 552
620 553 raw_diff, chunks, _stats = self._new_parse_lines(diff)
621 554 stats['binary'] = False
622 555 stats['added'] = _stats[0]
623 556 stats['deleted'] = _stats[1]
624 557 # explicit mark that it's a modified file
625 558 if op == OPS.MOD:
626 559 stats['ops'][MOD_FILENODE] = 'modified file'
627 560
628 561 except DiffLimitExceeded:
629 562 diff_container = lambda _diff: \
630 563 LimitedDiffContainer(
631 564 self.diff_limit, self.cur_diff_size, _diff)
632 565
633 566 limited_diff = True
634 567 chunks = []
635 568
636 569 else: # GIT format binary patch, or possibly empty diff
637 570 if head['bin_patch']:
638 571 # we have operation already extracted, but we mark simply
639 572 # it's a diff we wont show for binary files
640 573 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
641 574 chunks = []
642 575
643 576 # Hide content of deleted node by setting empty chunks
644 577 if chunks and not self.show_full_diff and op == OPS.DEL:
645 578 # if not full diff mode show deleted file contents
646 579 # TODO: anderson: if the view is not too big, there is no way
647 580 # to see the content of the file
648 581 chunks = []
649 582
650 583 chunks.insert(
651 584 0, [{'old_lineno': '',
652 585 'new_lineno': '',
653 586 'action': Action.CONTEXT,
654 587 'line': msg,
655 588 } for _op, msg in stats['ops'].iteritems()
656 589 if _op not in [MOD_FILENODE]])
657 590
658 591 original_filename = safe_unicode(head['a_path'])
659 592 _files.append({
660 593 'original_filename': original_filename,
661 594 'filename': safe_unicode(head['b_path']),
662 595 'old_revision': head['a_blob_id'],
663 596 'new_revision': head['b_blob_id'],
664 597 'chunks': chunks,
665 598 'raw_diff': safe_unicode(raw_diff),
666 599 'operation': op,
667 600 'stats': stats,
668 601 'exceeds_limit': exceeds_limit,
669 602 'is_limited_diff': limited_diff,
670 603 })
671 604
672 605 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
673 606 OPS.DEL: 2}.get(info['operation'])
674 607
675 608 return diff_container(sorted(_files, key=sorter))
676 609
677 610 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
678 611 def _parse_lines(self, diff):
679 612 """
680 613 Parse the diff an return data for the template.
681 614 """
682 615
683 616 lineiter = iter(diff)
684 617 stats = [0, 0]
685 618 chunks = []
686 619 raw_diff = []
687 620
688 621 try:
689 622 line = lineiter.next()
690 623
691 624 while line:
692 625 raw_diff.append(line)
693 626 lines = []
694 627 chunks.append(lines)
695 628
696 629 match = self._chunk_re.match(line)
697 630
698 631 if not match:
699 632 break
700 633
701 634 gr = match.groups()
702 635 (old_line, old_end,
703 636 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
704 637 old_line -= 1
705 638 new_line -= 1
706 639
707 640 context = len(gr) == 5
708 641 old_end += old_line
709 642 new_end += new_line
710 643
711 644 if context:
712 645 # skip context only if it's first line
713 646 if int(gr[0]) > 1:
714 647 lines.append({
715 648 'old_lineno': '...',
716 649 'new_lineno': '...',
717 650 'action': Action.CONTEXT,
718 651 'line': line,
719 652 })
720 653
721 654 line = lineiter.next()
722 655
723 656 while old_line < old_end or new_line < new_end:
724 657 command = ' '
725 658 if line:
726 659 command = line[0]
727 660
728 661 affects_old = affects_new = False
729 662
730 663 # ignore those if we don't expect them
731 664 if command in '#@':
732 665 continue
733 666 elif command == '+':
734 667 affects_new = True
735 668 action = Action.ADD
736 669 stats[0] += 1
737 670 elif command == '-':
738 671 affects_old = True
739 672 action = Action.DELETE
740 673 stats[1] += 1
741 674 else:
742 675 affects_old = affects_new = True
743 676 action = Action.UNMODIFIED
744 677
745 678 if not self._newline_marker.match(line):
746 679 old_line += affects_old
747 680 new_line += affects_new
748 681 lines.append({
749 682 'old_lineno': affects_old and old_line or '',
750 683 'new_lineno': affects_new and new_line or '',
751 684 'action': action,
752 685 'line': self._clean_line(line, command)
753 686 })
754 687 raw_diff.append(line)
755 688
756 689 line = lineiter.next()
757 690
758 691 if self._newline_marker.match(line):
759 692 # we need to append to lines, since this is not
760 693 # counted in the line specs of diff
761 694 lines.append({
762 695 'old_lineno': '...',
763 696 'new_lineno': '...',
764 697 'action': Action.CONTEXT,
765 698 'line': self._clean_line(line, command)
766 699 })
767 700
768 701 except StopIteration:
769 702 pass
770 703 return ''.join(raw_diff), chunks, stats
771 704
772 705 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
773 706 def _new_parse_lines(self, diff_iter):
774 707 """
775 708 Parse the diff an return data for the template.
776 709 """
777 710
778 711 stats = [0, 0]
779 712 chunks = []
780 713 raw_diff = []
781 714
782 715 diff_iter = imap(lambda s: safe_unicode(s), diff_iter)
783 716
784 717 try:
785 718 line = diff_iter.next()
786 719
787 720 while line:
788 721 raw_diff.append(line)
789 722 match = self._chunk_re.match(line)
790 723
791 724 if not match:
792 725 break
793 726
794 727 gr = match.groups()
795 728 (old_line, old_end,
796 729 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
797 730
798 731 lines = []
799 732 hunk = {
800 733 'section_header': gr[-1],
801 734 'source_start': old_line,
802 735 'source_length': old_end,
803 736 'target_start': new_line,
804 737 'target_length': new_end,
805 738 'lines': lines,
806 739 }
807 740 chunks.append(hunk)
808 741
809 742 old_line -= 1
810 743 new_line -= 1
811 744
812 745 context = len(gr) == 5
813 746 old_end += old_line
814 747 new_end += new_line
815 748
816 749 line = diff_iter.next()
817 750
818 751 while old_line < old_end or new_line < new_end:
819 752 command = ' '
820 753 if line:
821 754 command = line[0]
822 755
823 756 affects_old = affects_new = False
824 757
825 758 # ignore those if we don't expect them
826 759 if command in '#@':
827 760 continue
828 761 elif command == '+':
829 762 affects_new = True
830 763 action = Action.ADD
831 764 stats[0] += 1
832 765 elif command == '-':
833 766 affects_old = True
834 767 action = Action.DELETE
835 768 stats[1] += 1
836 769 else:
837 770 affects_old = affects_new = True
838 771 action = Action.UNMODIFIED
839 772
840 773 if not self._newline_marker.match(line):
841 774 old_line += affects_old
842 775 new_line += affects_new
843 776 lines.append({
844 777 'old_lineno': affects_old and old_line or '',
845 778 'new_lineno': affects_new and new_line or '',
846 779 'action': action,
847 780 'line': self._clean_line(line, command)
848 781 })
849 782 raw_diff.append(line)
850 783
851 784 line = diff_iter.next()
852 785
853 786 if self._newline_marker.match(line):
854 787 # we need to append to lines, since this is not
855 788 # counted in the line specs of diff
856 789 if affects_old:
857 790 action = Action.OLD_NO_NL
858 791 elif affects_new:
859 792 action = Action.NEW_NO_NL
860 793 else:
861 794 raise Exception('invalid context for no newline')
862 795
863 796 lines.append({
864 797 'old_lineno': None,
865 798 'new_lineno': None,
866 799 'action': action,
867 800 'line': self._clean_line(line, command)
868 801 })
869 802
870 803 except StopIteration:
871 804 pass
872 805
873 806 return ''.join(raw_diff), chunks, stats
874 807
875 808 def _safe_id(self, idstring):
876 809 """Make a string safe for including in an id attribute.
877 810
878 811 The HTML spec says that id attributes 'must begin with
879 812 a letter ([A-Za-z]) and may be followed by any number
880 813 of letters, digits ([0-9]), hyphens ("-"), underscores
881 814 ("_"), colons (":"), and periods (".")'. These regexps
882 815 are slightly over-zealous, in that they remove colons
883 816 and periods unnecessarily.
884 817
885 818 Whitespace is transformed into underscores, and then
886 819 anything which is not a hyphen or a character that
887 820 matches \w (alphanumerics and underscore) is removed.
888 821
889 822 """
890 823 # Transform all whitespace to underscore
891 824 idstring = re.sub(r'\s', "_", '%s' % idstring)
892 825 # Remove everything that is not a hyphen or a member of \w
893 826 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
894 827 return idstring
895 828
896 829 def prepare(self, inline_diff=True):
897 830 """
898 831 Prepare the passed udiff for HTML rendering.
899 832
900 833 :return: A list of dicts with diff information.
901 834 """
902 835 parsed = self._parser(inline_diff=inline_diff)
903 836 self.parsed = True
904 837 self.parsed_diff = parsed
905 838 return parsed
906 839
907 840 def as_raw(self, diff_lines=None):
908 841 """
909 842 Returns raw diff as a byte string
910 843 """
911 844 return self._diff.raw
912 845
913 846 def as_html(self, table_class='code-difftable', line_class='line',
914 847 old_lineno_class='lineno old', new_lineno_class='lineno new',
915 848 code_class='code', enable_comments=False, parsed_lines=None):
916 849 """
917 850 Return given diff as html table with customized css classes
918 851 """
852 # TODO(marcink): not sure how to pass in translator
853 # here in an efficient way, leave the _ for proper gettext extraction
854 _ = lambda s: s
855
919 856 def _link_to_if(condition, label, url):
920 857 """
921 858 Generates a link if condition is meet or just the label if not.
922 859 """
923 860
924 861 if condition:
925 862 return '''<a href="%(url)s" class="tooltip"
926 863 title="%(title)s">%(label)s</a>''' % {
927 864 'title': _('Click to select line'),
928 865 'url': url,
929 866 'label': label
930 867 }
931 868 else:
932 869 return label
933 870 if not self.parsed:
934 871 self.prepare()
935 872
936 873 diff_lines = self.parsed_diff
937 874 if parsed_lines:
938 875 diff_lines = parsed_lines
939 876
940 877 _html_empty = True
941 878 _html = []
942 879 _html.append('''<table class="%(table_class)s">\n''' % {
943 880 'table_class': table_class
944 881 })
945 882
946 883 for diff in diff_lines:
947 884 for line in diff['chunks']:
948 885 _html_empty = False
949 886 for change in line:
950 887 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
951 888 'lc': line_class,
952 889 'action': change['action']
953 890 })
954 891 anchor_old_id = ''
955 892 anchor_new_id = ''
956 893 anchor_old = "%(filename)s_o%(oldline_no)s" % {
957 894 'filename': self._safe_id(diff['filename']),
958 895 'oldline_no': change['old_lineno']
959 896 }
960 897 anchor_new = "%(filename)s_n%(oldline_no)s" % {
961 898 'filename': self._safe_id(diff['filename']),
962 899 'oldline_no': change['new_lineno']
963 900 }
964 901 cond_old = (change['old_lineno'] != '...' and
965 902 change['old_lineno'])
966 903 cond_new = (change['new_lineno'] != '...' and
967 904 change['new_lineno'])
968 905 if cond_old:
969 906 anchor_old_id = 'id="%s"' % anchor_old
970 907 if cond_new:
971 908 anchor_new_id = 'id="%s"' % anchor_new
972 909
973 910 if change['action'] != Action.CONTEXT:
974 911 anchor_link = True
975 912 else:
976 913 anchor_link = False
977 914
978 915 ###########################################################
979 916 # COMMENT ICONS
980 917 ###########################################################
981 918 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
982 919
983 920 if enable_comments and change['action'] != Action.CONTEXT:
984 921 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
985 922
986 923 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
987 924
988 925 ###########################################################
989 926 # OLD LINE NUMBER
990 927 ###########################################################
991 928 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
992 929 'a_id': anchor_old_id,
993 930 'olc': old_lineno_class
994 931 })
995 932
996 933 _html.append('''%(link)s''' % {
997 934 'link': _link_to_if(anchor_link, change['old_lineno'],
998 935 '#%s' % anchor_old)
999 936 })
1000 937 _html.append('''</td>\n''')
1001 938 ###########################################################
1002 939 # NEW LINE NUMBER
1003 940 ###########################################################
1004 941
1005 942 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
1006 943 'a_id': anchor_new_id,
1007 944 'nlc': new_lineno_class
1008 945 })
1009 946
1010 947 _html.append('''%(link)s''' % {
1011 948 'link': _link_to_if(anchor_link, change['new_lineno'],
1012 949 '#%s' % anchor_new)
1013 950 })
1014 951 _html.append('''</td>\n''')
1015 952 ###########################################################
1016 953 # CODE
1017 954 ###########################################################
1018 955 code_classes = [code_class]
1019 956 if (not enable_comments or
1020 957 change['action'] == Action.CONTEXT):
1021 958 code_classes.append('no-comment')
1022 959 _html.append('\t<td class="%s">' % ' '.join(code_classes))
1023 960 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
1024 961 'code': change['line']
1025 962 })
1026 963
1027 964 _html.append('''\t</td>''')
1028 965 _html.append('''\n</tr>\n''')
1029 966 _html.append('''</table>''')
1030 967 if _html_empty:
1031 968 return None
1032 969 return ''.join(_html)
1033 970
1034 971 def stat(self):
1035 972 """
1036 973 Returns tuple of added, and removed lines for this instance
1037 974 """
1038 975 return self.adds, self.removes
1039 976
1040 977 def get_context_of_line(
1041 978 self, path, diff_line=None, context_before=3, context_after=3):
1042 979 """
1043 980 Returns the context lines for the specified diff line.
1044 981
1045 982 :type diff_line: :class:`DiffLineNumber`
1046 983 """
1047 984 assert self.parsed, "DiffProcessor is not initialized."
1048 985
1049 986 if None not in diff_line:
1050 987 raise ValueError(
1051 988 "Cannot specify both line numbers: {}".format(diff_line))
1052 989
1053 990 file_diff = self._get_file_diff(path)
1054 991 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1055 992
1056 993 first_line_to_include = max(idx - context_before, 0)
1057 994 first_line_after_context = idx + context_after + 1
1058 995 context_lines = chunk[first_line_to_include:first_line_after_context]
1059 996
1060 997 line_contents = [
1061 998 _context_line(line) for line in context_lines
1062 999 if _is_diff_content(line)]
1063 1000 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1064 1001 # Once they are fixed, we can drop this line here.
1065 1002 if line_contents:
1066 1003 line_contents[-1] = (
1067 1004 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1068 1005 return line_contents
1069 1006
1070 1007 def find_context(self, path, context, offset=0):
1071 1008 """
1072 1009 Finds the given `context` inside of the diff.
1073 1010
1074 1011 Use the parameter `offset` to specify which offset the target line has
1075 1012 inside of the given `context`. This way the correct diff line will be
1076 1013 returned.
1077 1014
1078 1015 :param offset: Shall be used to specify the offset of the main line
1079 1016 within the given `context`.
1080 1017 """
1081 1018 if offset < 0 or offset >= len(context):
1082 1019 raise ValueError(
1083 1020 "Only positive values up to the length of the context "
1084 1021 "minus one are allowed.")
1085 1022
1086 1023 matches = []
1087 1024 file_diff = self._get_file_diff(path)
1088 1025
1089 1026 for chunk in file_diff['chunks']:
1090 1027 context_iter = iter(context)
1091 1028 for line_idx, line in enumerate(chunk):
1092 1029 try:
1093 1030 if _context_line(line) == context_iter.next():
1094 1031 continue
1095 1032 except StopIteration:
1096 1033 matches.append((line_idx, chunk))
1097 1034 context_iter = iter(context)
1098 1035
1099 1036 # Increment position and triger StopIteration
1100 1037 # if we had a match at the end
1101 1038 line_idx += 1
1102 1039 try:
1103 1040 context_iter.next()
1104 1041 except StopIteration:
1105 1042 matches.append((line_idx, chunk))
1106 1043
1107 1044 effective_offset = len(context) - offset
1108 1045 found_at_diff_lines = [
1109 1046 _line_to_diff_line_number(chunk[idx - effective_offset])
1110 1047 for idx, chunk in matches]
1111 1048
1112 1049 return found_at_diff_lines
1113 1050
1114 1051 def _get_file_diff(self, path):
1115 1052 for file_diff in self.parsed_diff:
1116 1053 if file_diff['filename'] == path:
1117 1054 break
1118 1055 else:
1119 1056 raise FileNotInDiffException("File {} not in diff".format(path))
1120 1057 return file_diff
1121 1058
1122 1059 def _find_chunk_line_index(self, file_diff, diff_line):
1123 1060 for chunk in file_diff['chunks']:
1124 1061 for idx, line in enumerate(chunk):
1125 1062 if line['old_lineno'] == diff_line.old:
1126 1063 return chunk, idx
1127 1064 if line['new_lineno'] == diff_line.new:
1128 1065 return chunk, idx
1129 1066 raise LineNotInDiffException(
1130 1067 "The line {} is not part of the diff.".format(diff_line))
1131 1068
1132 1069
1133 1070 def _is_diff_content(line):
1134 1071 return line['action'] in (
1135 1072 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1136 1073
1137 1074
1138 1075 def _context_line(line):
1139 1076 return (line['action'], line['line'])
1140 1077
1141 1078
1142 1079 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1143 1080
1144 1081
1145 1082 def _line_to_diff_line_number(line):
1146 1083 new_line_no = line['new_lineno'] or None
1147 1084 old_line_no = line['old_lineno'] or None
1148 1085 return DiffLineNumber(old=old_line_no, new=new_line_no)
1149 1086
1150 1087
1151 1088 class FileNotInDiffException(Exception):
1152 1089 """
1153 1090 Raised when the context for a missing file is requested.
1154 1091
1155 1092 If you request the context for a line in a file which is not part of the
1156 1093 given diff, then this exception is raised.
1157 1094 """
1158 1095
1159 1096
1160 1097 class LineNotInDiffException(Exception):
1161 1098 """
1162 1099 Raised when the context for a missing line is requested.
1163 1100
1164 1101 If you request the context for a line in a file and this line is not
1165 1102 part of the given diff, then this exception is raised.
1166 1103 """
1167 1104
1168 1105
1169 1106 class DiffLimitExceeded(Exception):
1170 1107 pass
@@ -1,64 +1,64 b''
1 1 import datetime
2 2 import decimal
3 3 import functools
4 4
5 5 import simplejson as json
6 6
7 7 from rhodecode.lib.datelib import is_aware
8 8
9 9 try:
10 10 import rhodecode.translation
11 11 except ImportError:
12 12 rhodecode = None
13 13
14 14 __all__ = ['json']
15 15
16 16
17 17 def _obj_dump(obj):
18 18 """
19 19 Custom function for dumping objects to JSON, if obj has __json__ attribute
20 20 or method defined it will be used for serialization
21 21
22 22 :param obj:
23 23 """
24 24
25 25 # See "Date Time String Format" in the ECMA-262 specification.
26 26 # some code borrowed from django 1.4
27 27 if isinstance(obj, set):
28 28 return list(obj)
29 29 elif isinstance(obj, datetime.datetime):
30 30 r = obj.isoformat()
31 31 if isinstance(obj.microsecond, (int, long)):
32 32 r = r[:23] + r[26:]
33 33 if r.endswith('+00:00'):
34 34 r = r[:-6] + 'Z'
35 35 return r
36 36 elif isinstance(obj, datetime.date):
37 37 return obj.isoformat()
38 38 elif isinstance(obj, datetime.time):
39 39 if is_aware(obj):
40 40 raise TypeError("Time-zone aware times are not JSON serializable")
41 41 r = obj.isoformat()
42 42 if isinstance(obj.microsecond, (int, long)):
43 43 r = r[:12]
44 44 return r
45 45 elif hasattr(obj, '__json__'):
46 46 if callable(obj.__json__):
47 47 return obj.__json__()
48 48 else:
49 49 return obj.__json__
50 50 elif isinstance(obj, decimal.Decimal):
51 51 return str(obj)
52 52 elif isinstance(obj, complex):
53 53 return [obj.real, obj.imag]
54 elif rhodecode and isinstance(obj, rhodecode.translation.LazyString):
54 elif rhodecode and isinstance(obj, rhodecode.translation._LazyString):
55 55 return obj.eval()
56 56 else:
57 57 raise TypeError(repr(obj) + " is not JSON serializable")
58 58
59 59
60 60 json.dumps = functools.partial(json.dumps, default=_obj_dump, use_decimal=False)
61 61 json.dump = functools.partial(json.dump, default=_obj_dump, use_decimal=False)
62 62
63 63 # alias for formatted json
64 64 formatted_json = functools.partial(json.dumps, indent=4, sort_keys=True)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now