##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r1610:1f5086cf merge stable
parent child Browse files
Show More

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

@@ -0,0 +1,14 b''
1 .. _git-lfs-loc:
2
3 Change the |git| LFS storage Location
4 -------------------------------------
5
6 |RCE| manages |git| LFS files from the following default location
7 :file:`/home/{user}/repos/.cache/lfs_store`. If you wish to change this, use
8 the following steps:
9
10 1. Open :menuselection:`Admin --> Settings --> VCS` as super-admin.
11
12 In section called `Git Settings` you can change where the LFS
13 objects should be stored.
14
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,162 b''
1 |RCE| 4.7.0 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2017-04-08
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Git: added support for Git LFS v2 protocol. RhodeCode now supports both
14 Mercurial Largefiles, and Git LFS for storing large binaries.
15 - Largefiles: detect Git LFS or Mercurial Largefiles objects in UI.
16 Those are now available for downloading together with showing their size.
17 - Files: Jupyter notebooks will be now rendered inside the file view. Including
18 MatJax support, and relative images.
19 - Files: render images inside the file view.
20 Instead of displaying binary message, render images icons and gifs
21 inside the file view page.
22 - Files: relative ULR support inside rendered files. It's now possible to
23 write Markup files and relative links will be handled from the RhodeCode
24 instance itself. Adds basic wiki functionality.
25 - Files: allow to show inline pdf in browser using embedded files from source code.
26 - Annotation: added shortcut links to browse the annotation view with previous
27 commits. Allows browsing history for each line from annotation view.
28 - Pull Requests: add explicit close action instead of close with status from
29 status selector. This allows closing of approved or rejected
30 pull requests, without performing a merge action.
31 - Authentication: LDAP now has an option to sync LDA groups using two
32 distinct ways. Either using rfc2307 or rfc2307bis. Increases compatibility
33 with different OpenLDAP and AD servers.
34 - Slack: updated slack integration to use the attachments for nicer formatting.
35 Added number of commits inside the message, changed UI for all Slack events.
36 - Authentication (EE edition only): added repository scope for VCS type auth
37 tokens. Each token can be now bound to particular repository for added security.
38 - User administration: added audit page to allow showing single user actions.
39 - API: implemented `get_user_audit_logs` method to fetch audit logs via API endpoint.
40 - User administration: It's now possible to edit user group membership from
41 user view.
42 - User groups administration: added managing and showing the group
43 synchronization in UI. It's now possible to enable manual group syncing on
44 already existing user groups from external sources such as LDAP/AD.
45 - Repositories: added new strip view allowing removing commits from repositories
46 via web interface for repository administrators.
47 - System Info: added info about workers and worker type.
48 Added more details about CPU. Expose workers of VCSServer in system info data.
49 Detect database migration errors.
50
51
52 General
53 ^^^^^^^
54
55 - Core: ported many views into pure pyramid code with python3.6 compatibility.
56 - Core: removed deprecated Pyro4 backend from Enterprise code.
57 - Maintenance: implemented maintenance view for Mercurial and GIT repositories.
58 For HG it will run `hg verify`, and for GIT a `git gc` command.
59 - Notifications: different approach with fixed/standard container. Floating
60 notifications no longer hide the menu when browsed on top of the page.
61 Also added option to remove single elements from stacked notifications.
62 - VCS server: exception-handling: better handling of remote exception and logging.
63 - VCS server: propagate hooks tracebacks to VCS server for easier debugging.
64 - Core: prevent `httplib3` logs to spam internal RhodeCode logs.
65 It often confuses people looking at those entries, misleading during debug.
66 - Mercurial: allow editing Largefiles store location from web interface.
67 - Git: allow editing GIT LFS store location from web interface.
68 - API: add get_method API call. This allows showing the method and it's parameter
69 from the CLI without reading the documentation.
70 In addition use it's mechanics to propose users other methods with close names
71 if the calling method is not found.
72 - UI: add timezone info into tooltips.
73 - Dependencies: bumped pyramid to 1.7.4
74 - Dependencies: bumped Mercurial version to 4.1.2
75
76
77 Security
78 ^^^^^^^^
79
80 - Hooks: added changes to propagate commit metadata on pre-push.
81 This allows easier implementation of checking hooks such as branch protection.
82 - Hooks: added new pretx hook to allow mercurial checks such as protected
83 branches, or force push.
84 - Auth: give owner of user group proper admin permissions to the user group.
85 This makes the behaviour consistent with repositories and repository groups.
86 And allows delegation of administration of those to other users.
87 - Password reset: strengthen security on password reset logic.
88 Generate token that has special password reset role.
89 Set 10 minute expiration for the token.
90 Add some logic to prevent brute forcing attacks.
91 Use more implicit messages to prevent user email discovery attacks.
92 - Core: added checks for password change for authenticated users in pure
93 Pyramid views. 2 views were still available and not forcing users to change
94 their passwords.
95 - Auth tokens: removed builtin auth-token for users.
96 Builtin token were non-removable, and always generated for new users. This
97 wasn't best practice for security as some users are strictly not allowed to
98 use tokens. From now on new users needs a new token generation in case they
99 want to use token based authentication.
100 - Auth tokens: don't generate builtin token for new users.
101 Also don't change them when password reset is made.
102 - Api: added last-activity into returned data of get_user api.
103
104
105 Performance
106 ^^^^^^^^^^^
107
108 - Mercurial: enabled new `Zstandard` compression algorithm available with
109 Mercurial 4.1.X. This allows faster, more CPU efficient clones when used
110 with new Mercurial clients.
111
112 - Users Admin: moved user admin to pyramid, and made it load users in chunks.
113 Fixed loading data to be lazy fetched, drastically improves speed of user
114 administration page in case of large amount of users.
115
116
117 Fixes
118 ^^^^^
119
120 - Search: goto commit search will now use a safe search option and never
121 throw any exceptions even if search is misconfigured
122 e.g. Elastic Search cluster is down.
123 - Events: fix a case for events called from API that couldn't fetch
124 registered user object.
125 - Comments: unlock submit if we use slash commands to set status.
126 - UI: fixed an issue with date of last change was not displayed correctly.
127 - Emails: added comment types (TODO/NOTE) into emails.
128 - Events: fix wrongly returned author data.
129 - Error middleware: read the instance title from cached object.
130 Reading from settings inside error handler can cause error hiding when
131 error_handler was caused by database errors.
132 - Pull requests: show version age component should use local dates instead of UTC.
133 - Pull requests: lock button when updating reviewers to forbid multi-submit
134 problems. Additionally fixed some small UI issues found in that view.
135 - Pull requests: forbid browsing versions on closed pull request.
136 - Pull requests: allow super-admins to delete pull requests instead of only owners.
137 - Diffs: support mercurial copy operation in diffs details.
138 - SVN: escape special chars to allow interactions with non-standard svn paths.
139 Path with special characters such as '#' will no longer trigger 404 errors.
140 - Data grids: fix some styling and processing text display.
141 - API: use consistent way to extract users, repos, repo groups and user groups
142 by id or name. Makes usage of Number vs String to differentiate if we pick
143 object ID or it's name this will allow editing of objects by either id or
144 it's name, including numeric string names.
145 - API: validate commit_id when using commit_comment API
146 - API: cleanup sessions enforce older_then must be a valid INT.
147
148
149 Upgrade notes
150 ^^^^^^^^^^^^^
151
152 - Auth-tokens: a builtin token will be migrated for all users into a custom
153 external token. We advise to inform users that the current builtin tokens
154 will now show as external ones. Builtin tokens were removed to allow expiring
155 ,or removing them. It's now possible to create users without any tokens.
156
157 From now on new users needs a new token generation in case they want to use
158 token based authentication.
159
160 - Hooks: we added via migration a pre transaction hook for Mercurial. If you're
161 using a custom code inside pre-push function of rcextensions make sure it
162 will not block your pushes.
@@ -0,0 +1,93 b''
1 .. _git-lfs-files:
2
3 |git| LFS Extension
4 ===================
5
6
7 Git Large File Storage (or LFS) is a new, open-source extension to Git that
8 aims to improve handling of large files. It does this by replacing large files
9 in your repository—such as graphics and videos—with simple text pointers.
10 |RC| Server includes an embedded LFS object store server, allowing storage of
11 large files without the need for an external object store.
12 Git LFS is disabled by default, globally, and for each individual repository.
13
14 .. note::
15
16 |RC| implements V2 API of Git LFS. Please make sure your git client is
17 using the latest version (2.0.X recommended) to leverage full feature set
18 of the V2 API.
19
20
21
22 Enabling Git LFS
23 ++++++++++++++++
24
25 Git LFS is disabled by default within |RC| Server.
26
27 To enable Git LFS Globally:
28
29 - Go to :menuselection:`Admin --> Settings --> VCS`
30
31 - Scroll down into `Git settings`
32
33 - Tick `Enable lfs extension`
34
35 - Save your settings.
36
37 Those settings apply globally to each repository that inherits from the defaults
38 You can leave `lfs extension` disabled globally, and only enable it per
39 repository that would use the lfs.
40
41
42 .. note::
43
44 You might want to adjust the global storage location at that point, however
45 we recommend leaving the default one created.
46
47
48 Installing and using the Git LFS command line client
49 ++++++++++++++++++++++++++++++++++++++++++++++++++++
50
51 Git LFS aims to integrate with the standard Git workflow as seamlessly
52 as possible. To push your first Git LFS files to an existing repository
53 Download and install the git-lfs command line client
54 Install the Git LFS filters::
55
56 git lfs install
57
58 This adds the following lines to the .gitconfig file located in your home directory::
59
60 [filter "lfs"]
61 clean = git-lfs clean %f
62 smudge = git-lfs smudge %f
63 required = true
64
65 The above change applies globally, so it is not necessary to run this for
66 each repository you work with. Choose the file types you would like LFS to
67 handle by executing the git lfs track command. The git lfs track command
68 creates or updates the .gitattributes file in your repository.
69 Change to your cloned repository, then execute git add to ensure updates
70 to the .gitattributes are later committed::
71
72 git lfs track "*.jpg"
73 git add .gitattributes
74
75 Add, commit, and push your changes as you normally would::
76
77 git add image.jpg
78 git commit -m "Added an image"
79 git push
80
81 When pushed, the Git tree updates to include a pointer to the file actual
82 file content. This pointer will include the SHA256 hash of the object and its
83 size in bytes. For example::
84
85 oid sha256:4fa32d6f9b1461c4a53618a47324ee43e36ce7ceaea2ad440cc811a7e6881be1
86 size 2580390
87
88
89 The object itself will be uploaded to a separate location via the Git LFS Batch API.
90 The transfer is validated and authorized by |RC| server itself.
91
92 If give repository has Git LFS disabled, a proper message will be sent back to
93 the client and upload of LFS objects will be forbidden.
@@ -0,0 +1,12 b''
1 diff -rup celery-2.2.10-orig/setup.py celery-2.2.10/setup.py
2 --- celery-2.2.10-orig/setup.py 2017-02-25 15:30:34.000000000 +0100
3 +++ celery-2.2.10/setup.py 2017-02-25 15:30:34.000000000 +0100
4 @@ -48,7 +48,7 @@ try:
5 except ImportError:
6 install_requires.append("importlib")
7 install_requires.extend([
8 - "python-dateutil>=1.5.0,<2.0.0",
9 + "python-dateutil>=1.5.0,<2.2.0",
10 "anyjson>=0.3.1",
11 "kombu>=1.1.2,<2.0.0",
12 "pyparsing>=1.5.0,<2.0.0",
@@ -0,0 +1,58 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
25
26
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestGetMethod(object):
29 def test_get_methods_no_matches(self):
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
31 response = api_call(self.app, params)
32
33 expected = []
34 assert_ok(id_, expected, given=response.body)
35
36 def test_get_methods(self):
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 response = api_call(self.app, params)
39
40 expected = ['changeset_comment', 'comment_pull_request',
41 'comment_commit']
42 assert_ok(id_, expected, given=response.body)
43
44 def test_get_methods_on_single_match(self):
45 id_, params = build_data(self.apikey, 'get_method', pattern='*comment_commit*')
46 response = api_call(self.app, params)
47
48 expected = ['comment_commit',
49 {'apiuser': '<RequiredType>',
50 'comment_type': "<Optional:u'note'>",
51 'commit_id': '<RequiredType>',
52 'message': '<RequiredType>',
53 'repoid': '<RequiredType>',
54 'request': '<RequiredType>',
55 'resolves_comment_id': '<Optional:None>',
56 'status': '<Optional:None>',
57 'userid': '<Optional:<OptionalAttr:apiuser>>'}]
58 assert_ok(id_, expected, given=response.body)
1 NO CONTENT: new file 100644
@@ -0,0 +1,163 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import time
22 import logging
23 from pylons import tmpl_context as c
24 from pyramid.httpexceptions import HTTPFound
25
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int
28 from rhodecode.model import repo
29 from rhodecode.model.db import User
30 from rhodecode.model.scm import ScmModel
31
32 log = logging.getLogger(__name__)
33
34
35 ADMIN_PREFIX = '/_admin'
36 STATIC_FILE_PREFIX = '/_static'
37
38
39 class TemplateArgs(StrictAttributeDict):
40 pass
41
42
43 class BaseAppView(object):
44
45 def __init__(self, context, request):
46 self.request = request
47 self.context = context
48 self.session = request.session
49 self._rhodecode_user = request.user # auth user
50 self._rhodecode_db_user = self._rhodecode_user.get_instance()
51 self._maybe_needs_password_change(
52 request.matched_route.name, self._rhodecode_db_user)
53
54 def _maybe_needs_password_change(self, view_name, user_obj):
55 log.debug('Checking if user %s needs password change on view %s',
56 user_obj, view_name)
57 skip_user_views = [
58 'logout', 'login',
59 'my_account_password', 'my_account_password_update'
60 ]
61
62 if not user_obj:
63 return
64
65 if user_obj.username == User.DEFAULT_USER:
66 return
67
68 now = time.time()
69 should_change = user_obj.user_data.get('force_password_change')
70 change_after = safe_int(should_change) or 0
71 if should_change and now > change_after:
72 log.debug('User %s requires password change', user_obj)
73 h.flash('You are required to change your password', 'warning',
74 ignore_duplicate=True)
75
76 if view_name not in skip_user_views:
77 raise HTTPFound(
78 self.request.route_path('my_account_password'))
79
80 def _get_local_tmpl_context(self):
81 c = TemplateArgs()
82 c.auth_user = self.request.user
83 return c
84
85 def _register_global_c(self, tmpl_args):
86 """
87 Registers attributes to pylons global `c`
88 """
89 # TODO(marcink): remove once pyramid migration is finished
90 for k, v in tmpl_args.items():
91 setattr(c, k, v)
92
93 def _get_template_context(self, tmpl_args):
94 self._register_global_c(tmpl_args)
95
96 local_tmpl_args = {
97 'defaults': {},
98 'errors': {},
99 }
100 local_tmpl_args.update(tmpl_args)
101 return local_tmpl_args
102
103 def load_default_context(self):
104 """
105 example:
106
107 def load_default_context(self):
108 c = self._get_local_tmpl_context()
109 c.custom_var = 'foobar'
110 self._register_global_c(c)
111 return c
112 """
113 raise NotImplementedError('Needs implementation in view class')
114
115
116 class RepoAppView(BaseAppView):
117
118 def __init__(self, context, request):
119 super(RepoAppView, self).__init__(context, request)
120 self.db_repo = request.db_repo
121 self.db_repo_name = self.db_repo.repo_name
122 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
123
124 def _get_local_tmpl_context(self):
125 c = super(RepoAppView, self)._get_local_tmpl_context()
126 # register common vars for this type of view
127 c.rhodecode_db_repo = self.db_repo
128 c.repo_name = self.db_repo_name
129 c.repository_pull_requests = self.db_repo_pull_requests
130 return c
131
132
133 class RepoRoutePredicate(object):
134 def __init__(self, val, config):
135 self.val = val
136
137 def text(self):
138 return 'repo_route = %s' % self.val
139
140 phash = text
141
142 def __call__(self, info, request):
143 repo_name = info['match']['repo_name']
144 repo_model = repo.RepoModel()
145 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
146 # if we match quickly from database, short circuit the operation,
147 # and validate repo based on the type.
148 if by_name_match:
149 # register this as request object we can re-use later
150 request.db_repo = by_name_match
151 return True
152
153 by_id_match = repo_model.get_repo_by_id(repo_name)
154 if by_id_match:
155 request.db_repo = by_id_match
156 return True
157
158 return False
159
160
161 def includeme(config):
162 config.add_route_predicate(
163 'repo_route', RepoRoutePredicate)
1 NO CONTENT: new file 100644
@@ -0,0 +1,142 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.model.db import User, UserApiKeys
24
25 from rhodecode.tests import (
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
27 from rhodecode.tests.fixture import Fixture
28
29 fixture = Fixture()
30
31
32 def route_path(name, params=None, **kwargs):
33 import urllib
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'users':
38 ADMIN_PREFIX + '/users',
39 'users_data':
40 ADMIN_PREFIX + '/users_data',
41 'edit_user_auth_tokens':
42 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
43 'edit_user_auth_tokens_add':
44 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
45 'edit_user_auth_tokens_delete':
46 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
47 }[name].format(**kwargs)
48
49 if params:
50 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
51 return base_url
52
53
54 class TestAdminUsersView(TestController):
55
56 def test_show_users(self):
57 self.log_user()
58 self.app.get(route_path('users'))
59
60 def test_show_users_data(self, xhr_header):
61 self.log_user()
62 response = self.app.get(route_path(
63 'users_data'), extra_environ=xhr_header)
64
65 all_users = User.query().filter(
66 User.username != User.DEFAULT_USER).count()
67 assert response.json['recordsTotal'] == all_users
68
69 def test_show_users_data_filtered(self, xhr_header):
70 self.log_user()
71 response = self.app.get(route_path(
72 'users_data', params={'search[value]': 'empty_search'}),
73 extra_environ=xhr_header)
74
75 all_users = User.query().filter(
76 User.username != User.DEFAULT_USER).count()
77 assert response.json['recordsTotal'] == all_users
78 assert response.json['recordsFiltered'] == 0
79
80 def test_auth_tokens_default_user(self):
81 self.log_user()
82 user = User.get_default_user()
83 response = self.app.get(
84 route_path('edit_user_auth_tokens', user_id=user.user_id),
85 status=302)
86
87 def test_auth_tokens(self):
88 self.log_user()
89
90 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
91 response = self.app.get(
92 route_path('edit_user_auth_tokens', user_id=user.user_id))
93 for token in user.auth_tokens:
94 response.mustcontain(token)
95 response.mustcontain('never')
96
97 @pytest.mark.parametrize("desc, lifetime", [
98 ('forever', -1),
99 ('5mins', 60*5),
100 ('30days', 60*60*24*30),
101 ])
102 def test_add_auth_token(self, desc, lifetime, user_util):
103 self.log_user()
104 user = user_util.create_user()
105 user_id = user.user_id
106
107 response = self.app.post(
108 route_path('edit_user_auth_tokens_add', user_id=user_id),
109 {'description': desc, 'lifetime': lifetime,
110 'csrf_token': self.csrf_token})
111 assert_session_flash(response, 'Auth token successfully created')
112
113 response = response.follow()
114 user = User.get(user_id)
115 for auth_token in user.auth_tokens:
116 response.mustcontain(auth_token)
117
118 def test_delete_auth_token(self, user_util):
119 self.log_user()
120 user = user_util.create_user()
121 user_id = user.user_id
122 keys = user.extra_auth_tokens
123 assert 2 == len(keys)
124
125 response = self.app.post(
126 route_path('edit_user_auth_tokens_add', user_id=user_id),
127 {'description': 'desc', 'lifetime': -1,
128 'csrf_token': self.csrf_token})
129 assert_session_flash(response, 'Auth token successfully created')
130 response.follow()
131
132 # now delete our key
133 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
134 assert 3 == len(keys)
135
136 response = self.app.post(
137 route_path('edit_user_auth_tokens_delete', user_id=user_id),
138 {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
139
140 assert_session_flash(response, 'Auth token successfully deleted')
141 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
142 assert 2 == len(keys)
@@ -0,0 +1,317 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25
26 from rhodecode.lib.helpers import Page
27 from rhodecode_tools.lib.ext_json import json
28
29 from rhodecode.apps._base import BaseAppView
30 from rhodecode.lib.auth import (
31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib.utils import PartialRenderer
34 from rhodecode.lib.utils2 import safe_int, safe_unicode
35 from rhodecode.model.auth_token import AuthTokenModel
36 from rhodecode.model.user import UserModel
37 from rhodecode.model.user_group import UserGroupModel
38 from rhodecode.model.db import User, or_
39 from rhodecode.model.meta import Session
40
41 log = logging.getLogger(__name__)
42
43
44 class AdminUsersView(BaseAppView):
45 ALLOW_SCOPED_TOKENS = False
46 """
47 This view has alternative version inside EE, if modified please take a look
48 in there as well.
49 """
50
51 def load_default_context(self):
52 c = self._get_local_tmpl_context()
53 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
54 self._register_global_c(c)
55 return c
56
57 def _redirect_for_default_user(self, username):
58 _ = self.request.translate
59 if username == User.DEFAULT_USER:
60 h.flash(_("You can't edit this user"), category='warning')
61 # TODO(marcink): redirect to 'users' admin panel once this
62 # is a pyramid view
63 raise HTTPFound('/')
64
65 def _extract_ordering(self, request):
66 column_index = safe_int(request.GET.get('order[0][column]'))
67 order_dir = request.GET.get(
68 'order[0][dir]', 'desc')
69 order_by = request.GET.get(
70 'columns[%s][data][sort]' % column_index, 'name_raw')
71
72 # translate datatable to DB columns
73 order_by = {
74 'first_name': 'name',
75 'last_name': 'lastname',
76 }.get(order_by) or order_by
77
78 search_q = request.GET.get('search[value]')
79 return search_q, order_by, order_dir
80
81 def _extract_chunk(self, request):
82 start = safe_int(request.GET.get('start'), 0)
83 length = safe_int(request.GET.get('length'), 25)
84 draw = safe_int(request.GET.get('draw'))
85 return draw, start, length
86
87 @HasPermissionAllDecorator('hg.admin')
88 @view_config(
89 route_name='users', request_method='GET',
90 renderer='rhodecode:templates/admin/users/users.mako')
91 def users_list(self):
92 c = self.load_default_context()
93 return self._get_template_context(c)
94
95 @HasPermissionAllDecorator('hg.admin')
96 @view_config(
97 # renderer defined below
98 route_name='users_data', request_method='GET', renderer='json',
99 xhr=True)
100 def users_list_data(self):
101 draw, start, limit = self._extract_chunk(self.request)
102 search_q, order_by, order_dir = self._extract_ordering(self.request)
103
104 _render = PartialRenderer('data_table/_dt_elements.mako')
105
106 def user_actions(user_id, username):
107 return _render("user_actions", user_id, username)
108
109 users_data_total_count = User.query()\
110 .filter(User.username != User.DEFAULT_USER) \
111 .count()
112
113 # json generate
114 base_q = User.query().filter(User.username != User.DEFAULT_USER)
115
116 if search_q:
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 base_q = base_q.filter(or_(
119 User.username.ilike(like_expression),
120 User._email.ilike(like_expression),
121 User.name.ilike(like_expression),
122 User.lastname.ilike(like_expression),
123 ))
124
125 users_data_total_filtered_count = base_q.count()
126
127 sort_col = getattr(User, order_by, None)
128 if sort_col and order_dir == 'asc':
129 base_q = base_q.order_by(sort_col.asc().nullslast())
130 elif sort_col:
131 base_q = base_q.order_by(sort_col.desc().nullslast())
132
133 base_q = base_q.offset(start).limit(limit)
134 users_list = base_q.all()
135
136 users_data = []
137 for user in users_list:
138 users_data.append({
139 "username": h.gravatar_with_user(user.username),
140 "email": user.email,
141 "first_name": h.escape(user.name),
142 "last_name": h.escape(user.lastname),
143 "last_login": h.format_date(user.last_login),
144 "last_activity": h.format_date(user.last_activity),
145 "active": h.bool2icon(user.active),
146 "active_raw": user.active,
147 "admin": h.bool2icon(user.admin),
148 "extern_type": user.extern_type,
149 "extern_name": user.extern_name,
150 "action": user_actions(user.user_id, user.username),
151 })
152
153 data = ({
154 'draw': draw,
155 'data': users_data,
156 'recordsTotal': users_data_total_count,
157 'recordsFiltered': users_data_total_filtered_count,
158 })
159
160 return data
161
162 @LoginRequired()
163 @HasPermissionAllDecorator('hg.admin')
164 @view_config(
165 route_name='edit_user_auth_tokens', request_method='GET',
166 renderer='rhodecode:templates/admin/users/user_edit.mako')
167 def auth_tokens(self):
168 _ = self.request.translate
169 c = self.load_default_context()
170
171 user_id = self.request.matchdict.get('user_id')
172 c.user = User.get_or_404(user_id, pyramid_exc=True)
173 self._redirect_for_default_user(c.user.username)
174
175 c.active = 'auth_tokens'
176
177 c.lifetime_values = [
178 (str(-1), _('forever')),
179 (str(5), _('5 minutes')),
180 (str(60), _('1 hour')),
181 (str(60 * 24), _('1 day')),
182 (str(60 * 24 * 30), _('1 month')),
183 ]
184 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
185 c.role_values = [
186 (x, AuthTokenModel.cls._get_role_name(x))
187 for x in AuthTokenModel.cls.ROLES]
188 c.role_options = [(c.role_values, _("Role"))]
189 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
190 c.user.user_id, show_expired=True)
191 return self._get_template_context(c)
192
193 def maybe_attach_token_scope(self, token):
194 # implemented in EE edition
195 pass
196
197 @LoginRequired()
198 @HasPermissionAllDecorator('hg.admin')
199 @CSRFRequired()
200 @view_config(
201 route_name='edit_user_auth_tokens_add', request_method='POST')
202 def auth_tokens_add(self):
203 _ = self.request.translate
204 c = self.load_default_context()
205
206 user_id = self.request.matchdict.get('user_id')
207 c.user = User.get_or_404(user_id, pyramid_exc=True)
208 self._redirect_for_default_user(c.user.username)
209
210 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
211 description = self.request.POST.get('description')
212 role = self.request.POST.get('role')
213
214 token = AuthTokenModel().create(
215 c.user.user_id, description, lifetime, role)
216 self.maybe_attach_token_scope(token)
217 Session().commit()
218
219 h.flash(_("Auth token successfully created"), category='success')
220 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
221
222 @LoginRequired()
223 @HasPermissionAllDecorator('hg.admin')
224 @CSRFRequired()
225 @view_config(
226 route_name='edit_user_auth_tokens_delete', request_method='POST')
227 def auth_tokens_delete(self):
228 _ = self.request.translate
229 c = self.load_default_context()
230
231 user_id = self.request.matchdict.get('user_id')
232 c.user = User.get_or_404(user_id, pyramid_exc=True)
233 self._redirect_for_default_user(c.user.username)
234
235 del_auth_token = self.request.POST.get('del_auth_token')
236
237 if del_auth_token:
238 AuthTokenModel().delete(del_auth_token, c.user.user_id)
239 Session().commit()
240 h.flash(_("Auth token successfully deleted"), category='success')
241
242 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
243
244 @LoginRequired()
245 @HasPermissionAllDecorator('hg.admin')
246 @view_config(
247 route_name='edit_user_groups_management', request_method='GET',
248 renderer='rhodecode:templates/admin/users/user_edit.mako')
249 def groups_management(self):
250 c = self.load_default_context()
251
252 user_id = self.request.matchdict.get('user_id')
253 c.user = User.get_or_404(user_id, pyramid_exc=True)
254 c.data = c.user.group_member
255 self._redirect_for_default_user(c.user.username)
256 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
257 c.groups = json.dumps(groups)
258 c.active = 'groups'
259
260 return self._get_template_context(c)
261
262 @LoginRequired()
263 @HasPermissionAllDecorator('hg.admin')
264 @view_config(
265 route_name='edit_user_groups_management_updates', request_method='POST')
266 def groups_management_updates(self):
267 _ = self.request.translate
268 c = self.load_default_context()
269
270 user_id = self.request.matchdict.get('user_id')
271 c.user = User.get_or_404(user_id, pyramid_exc=True)
272 self._redirect_for_default_user(c.user.username)
273
274 users_groups = set(self.request.POST.getall('users_group_id'))
275 users_groups_model = []
276
277 for ugid in users_groups:
278 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
279 user_group_model = UserGroupModel()
280 user_group_model.change_groups(c.user, users_groups_model)
281
282 Session().commit()
283 c.active = 'user_groups_management'
284 h.flash(_("Groups successfully changed"), category='success')
285
286 return HTTPFound(h.route_path(
287 'edit_user_groups_management', user_id=user_id))
288
289 @LoginRequired()
290 @HasPermissionAllDecorator('hg.admin')
291 @view_config(
292 route_name='edit_user_audit_logs', request_method='GET',
293 renderer='rhodecode:templates/admin/users/user_edit.mako')
294 def user_audit_logs(self):
295 _ = self.request.translate
296 c = self.load_default_context()
297
298 user_id = self.request.matchdict.get('user_id')
299 c.user = User.get_or_404(user_id, pyramid_exc=True)
300 self._redirect_for_default_user(c.user.username)
301 c.active = 'audit'
302
303 p = safe_int(self.request.GET.get('page', 1), 1)
304
305 filter_term = self.request.GET.get('filter')
306 c.user_log = UserModel().get_user_log(c.user, filter_term)
307
308 def url_generator(**kw):
309 if filter_term:
310 kw['filter'] = filter_term
311 return self.request.current_route_path(_query=kw)
312
313 c.user_log = Page(c.user_log, page=p, items_per_page=10,
314 url=url_generator)
315 c.filter_term = filter_term
316 return self._get_template_context(c)
317
@@ -0,0 +1,52 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 from rhodecode.apps._base import ADMIN_PREFIX
23
24
25 def includeme(config):
26
27 config.add_route(
28 name='my_account_profile',
29 pattern=ADMIN_PREFIX + '/my_account/profile')
30
31 config.add_route(
32 name='my_account_password',
33 pattern=ADMIN_PREFIX + '/my_account/password')
34
35 config.add_route(
36 name='my_account_password_update',
37 pattern=ADMIN_PREFIX + '/my_account/password')
38
39 config.add_route(
40 name='my_account_auth_tokens',
41 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
42 config.add_route(
43 name='my_account_auth_tokens_add',
44 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new',
45 )
46 config.add_route(
47 name='my_account_auth_tokens_delete',
48 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete',
49 )
50
51 # Scan module for configuration decorators.
52 config.scan()
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,111 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.model.db import User
25 from rhodecode.tests import (
26 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
27 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.utils import AssertResponse
30
31 fixture = Fixture()
32
33
34 def route_path(name, **kwargs):
35 return {
36 'my_account_auth_tokens':
37 ADMIN_PREFIX + '/my_account/auth_tokens',
38 'my_account_auth_tokens_add':
39 ADMIN_PREFIX + '/my_account/auth_tokens/new',
40 'my_account_auth_tokens_delete':
41 ADMIN_PREFIX + '/my_account/auth_tokens/delete',
42 }[name].format(**kwargs)
43
44
45 class TestMyAccountAuthTokens(TestController):
46
47 def test_my_account_auth_tokens(self):
48 usr = self.log_user('test_regular2', 'test12')
49 user = User.get(usr['user_id'])
50 response = self.app.get(route_path('my_account_auth_tokens'))
51 for token in user.auth_tokens:
52 response.mustcontain(token)
53 response.mustcontain('never')
54
55 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
56 user = user_util.create_user(password='qweqwe')
57 self.log_user(user.username, 'qweqwe')
58
59 self.app.post(
60 route_path('my_account_auth_tokens_add'),
61 {'description': 'desc', 'lifetime': -1}, status=403)
62
63 @pytest.mark.parametrize("desc, lifetime", [
64 ('forever', -1),
65 ('5mins', 60*5),
66 ('30days', 60*60*24*30),
67 ])
68 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
69 user = user_util.create_user(password='qweqwe')
70 user_id = user.user_id
71 self.log_user(user.username, 'qweqwe')
72
73 response = self.app.post(
74 route_path('my_account_auth_tokens_add'),
75 {'description': desc, 'lifetime': lifetime,
76 'csrf_token': self.csrf_token})
77 assert_session_flash(response, 'Auth token successfully created')
78
79 response = response.follow()
80 user = User.get(user_id)
81 for auth_token in user.auth_tokens:
82 response.mustcontain(auth_token)
83
84 def test_my_account_delete_auth_token(self, user_util):
85 user = user_util.create_user(password='qweqwe')
86 user_id = user.user_id
87 self.log_user(user.username, 'qweqwe')
88
89 user = User.get(user_id)
90 keys = user.extra_auth_tokens
91 assert 2 == len(keys)
92
93 response = self.app.post(
94 route_path('my_account_auth_tokens_add'),
95 {'description': 'desc', 'lifetime': -1,
96 'csrf_token': self.csrf_token})
97 assert_session_flash(response, 'Auth token successfully created')
98 response.follow()
99
100 user = User.get(user_id)
101 keys = user.extra_auth_tokens
102 assert 3 == len(keys)
103
104 response = self.app.post(
105 route_path('my_account_auth_tokens_delete'),
106 {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
107 assert_session_flash(response, 'Auth token successfully deleted')
108
109 user = User.get(user_id)
110 keys = user.extra_auth_tokens
111 assert 2 == len(keys)
@@ -0,0 +1,137 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 import mock
23
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib import helpers as h
26 from rhodecode.lib.auth import check_password
27 from rhodecode.model.meta import Session
28 from rhodecode.model.user import UserModel
29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.fixture import Fixture, TestController, error_function
31
32 fixture = Fixture()
33
34
35 def route_path(name, **kwargs):
36 return {
37 'home': '/',
38 'my_account_password':
39 ADMIN_PREFIX + '/my_account/password',
40 }[name].format(**kwargs)
41
42
43 test_user_1 = 'testme'
44 test_user_1_password = '0jd83nHNS/d23n'
45
46
47 class TestMyAccountPassword(TestController):
48 def test_valid_change_password(self, user_util):
49 new_password = 'my_new_valid_password'
50 user = user_util.create_user(password=test_user_1_password)
51 self.log_user(user.username, test_user_1_password)
52
53 form_data = [
54 ('current_password', test_user_1_password),
55 ('__start__', 'new_password:mapping'),
56 ('new_password', new_password),
57 ('new_password-confirm', new_password),
58 ('__end__', 'new_password:mapping'),
59 ('csrf_token', self.csrf_token),
60 ]
61 response = self.app.post(route_path('my_account_password'), form_data).follow()
62 assert 'Successfully updated password' in response
63
64 # check_password depends on user being in session
65 Session().add(user)
66 try:
67 assert check_password(new_password, user.password)
68 finally:
69 Session().expunge(user)
70
71 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
72 ('', 'abcdef123', 'abcdef123'),
73 ('wrong_pw', 'abcdef123', 'abcdef123'),
74 (test_user_1_password, test_user_1_password, test_user_1_password),
75 (test_user_1_password, '', ''),
76 (test_user_1_password, 'abcdef123', ''),
77 (test_user_1_password, '', 'abcdef123'),
78 (test_user_1_password, 'not_the', 'same_pw'),
79 (test_user_1_password, 'short', 'short'),
80 ])
81 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
82 user_util):
83 user = user_util.create_user(password=test_user_1_password)
84 self.log_user(user.username, test_user_1_password)
85
86 form_data = [
87 ('current_password', current_pw),
88 ('__start__', 'new_password:mapping'),
89 ('new_password', new_pw),
90 ('new_password-confirm', confirm_pw),
91 ('__end__', 'new_password:mapping'),
92 ('csrf_token', self.csrf_token),
93 ]
94 response = self.app.post(route_path('my_account_password'), form_data)
95
96 assert_response = response.assert_response()
97 assert assert_response.get_elements('.error-block')
98
99 @mock.patch.object(UserModel, 'update_user', error_function)
100 def test_invalid_change_password_exception(self, user_util):
101 user = user_util.create_user(password=test_user_1_password)
102 self.log_user(user.username, test_user_1_password)
103
104 form_data = [
105 ('current_password', test_user_1_password),
106 ('__start__', 'new_password:mapping'),
107 ('new_password', '123456'),
108 ('new_password-confirm', '123456'),
109 ('__end__', 'new_password:mapping'),
110 ('csrf_token', self.csrf_token),
111 ]
112 response = self.app.post(route_path('my_account_password'), form_data)
113 assert_session_flash(
114 response, 'Error occurred during update of user password')
115
116 def test_password_is_updated_in_session_on_password_change(self, user_util):
117 old_password = 'abcdef123'
118 new_password = 'abcdef124'
119
120 user = user_util.create_user(password=old_password)
121 session = self.log_user(user.username, old_password)
122 old_password_hash = session['password']
123
124 form_data = [
125 ('current_password', old_password),
126 ('__start__', 'new_password:mapping'),
127 ('new_password', new_password),
128 ('new_password-confirm', new_password),
129 ('__end__', 'new_password:mapping'),
130 ('csrf_token', self.csrf_token),
131 ]
132 self.app.post(route_path('my_account_password'), form_data)
133
134 response = self.app.get(route_path('home'))
135 new_password_hash = response.session['rhodecode_user']['password']
136
137 assert old_password_hash != new_password_hash No newline at end of file
@@ -0,0 +1,55 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.tests import (
25 TestController, TEST_USER_ADMIN_LOGIN,
26 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
27 from rhodecode.tests.fixture import Fixture
28
29 fixture = Fixture()
30
31
32 def route_path(name, **kwargs):
33 return {
34 'my_account':
35 ADMIN_PREFIX + '/my_account/profile',
36 }[name].format(**kwargs)
37
38
39 class TestMyAccountProfile(TestController):
40
41 def test_my_account(self):
42 self.log_user()
43 response = self.app.get(route_path('my_account'))
44
45 response.mustcontain(TEST_USER_ADMIN_LOGIN)
46 response.mustcontain('href="/_admin/my_account/edit"')
47 response.mustcontain('Photo')
48
49 def test_my_account_regular_user(self):
50 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
51 response = self.app.get(route_path('my_account'))
52
53 response.mustcontain(TEST_USER_REGULAR_LOGIN)
54 response.mustcontain('href="/_admin/my_account/edit"')
55 response.mustcontain('Photo')
@@ -0,0 +1,194 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode import forms
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib.utils2 import safe_int, md5
31 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.meta import Session
33 from rhodecode.model.user import UserModel
34 from rhodecode.model.validation_schema.schemas import user_schema
35
36 log = logging.getLogger(__name__)
37
38
39 class MyAccountView(BaseAppView):
40 ALLOW_SCOPED_TOKENS = False
41 """
42 This view has alternative version inside EE, if modified please take a look
43 in there as well.
44 """
45
46 def load_default_context(self):
47 c = self._get_local_tmpl_context()
48 c.user = c.auth_user.get_instance()
49 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
50 self._register_global_c(c)
51 return c
52
53 @LoginRequired()
54 @NotAnonymous()
55 @view_config(
56 route_name='my_account_profile', request_method='GET',
57 renderer='rhodecode:templates/admin/my_account/my_account.mako')
58 def my_account_profile(self):
59 c = self.load_default_context()
60 c.active = 'profile'
61 return self._get_template_context(c)
62
63 @LoginRequired()
64 @NotAnonymous()
65 @view_config(
66 route_name='my_account_password', request_method='GET',
67 renderer='rhodecode:templates/admin/my_account/my_account.mako')
68 def my_account_password(self):
69 c = self.load_default_context()
70 c.active = 'password'
71 c.extern_type = c.user.extern_type
72
73 schema = user_schema.ChangePasswordSchema().bind(
74 username=c.user.username)
75
76 form = forms.Form(
77 schema, buttons=(forms.buttons.save, forms.buttons.reset))
78
79 c.form = form
80 return self._get_template_context(c)
81
82 @LoginRequired()
83 @NotAnonymous()
84 @CSRFRequired()
85 @view_config(
86 route_name='my_account_password', request_method='POST',
87 renderer='rhodecode:templates/admin/my_account/my_account.mako')
88 def my_account_password_update(self):
89 _ = self.request.translate
90 c = self.load_default_context()
91 c.active = 'password'
92 c.extern_type = c.user.extern_type
93
94 schema = user_schema.ChangePasswordSchema().bind(
95 username=c.user.username)
96
97 form = forms.Form(
98 schema, buttons=(forms.buttons.save, forms.buttons.reset))
99
100 if c.extern_type != 'rhodecode':
101 raise HTTPFound(self.request.route_path('my_account_password'))
102
103 controls = self.request.POST.items()
104 try:
105 valid_data = form.validate(controls)
106 UserModel().update_user(c.user.user_id, **valid_data)
107 c.user.update_userdata(force_password_change=False)
108 Session().commit()
109 except forms.ValidationFailure as e:
110 c.form = e
111 return self._get_template_context(c)
112
113 except Exception:
114 log.exception("Exception updating password")
115 h.flash(_('Error occurred during update of user password'),
116 category='error')
117 else:
118 instance = c.auth_user.get_instance()
119 self.session.setdefault('rhodecode_user', {}).update(
120 {'password': md5(instance.password)})
121 self.session.save()
122 h.flash(_("Successfully updated password"), category='success')
123
124 raise HTTPFound(self.request.route_path('my_account_password'))
125
126 @LoginRequired()
127 @NotAnonymous()
128 @view_config(
129 route_name='my_account_auth_tokens', request_method='GET',
130 renderer='rhodecode:templates/admin/my_account/my_account.mako')
131 def my_account_auth_tokens(self):
132 _ = self.request.translate
133
134 c = self.load_default_context()
135 c.active = 'auth_tokens'
136
137 c.lifetime_values = [
138 (str(-1), _('forever')),
139 (str(5), _('5 minutes')),
140 (str(60), _('1 hour')),
141 (str(60 * 24), _('1 day')),
142 (str(60 * 24 * 30), _('1 month')),
143 ]
144 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
145 c.role_values = [
146 (x, AuthTokenModel.cls._get_role_name(x))
147 for x in AuthTokenModel.cls.ROLES]
148 c.role_options = [(c.role_values, _("Role"))]
149 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
150 c.user.user_id, show_expired=True)
151 return self._get_template_context(c)
152
153 def maybe_attach_token_scope(self, token):
154 # implemented in EE edition
155 pass
156
157 @LoginRequired()
158 @NotAnonymous()
159 @CSRFRequired()
160 @view_config(
161 route_name='my_account_auth_tokens_add', request_method='POST')
162 def my_account_auth_tokens_add(self):
163 _ = self.request.translate
164 c = self.load_default_context()
165
166 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
167 description = self.request.POST.get('description')
168 role = self.request.POST.get('role')
169
170 token = AuthTokenModel().create(
171 c.user.user_id, description, lifetime, role)
172 self.maybe_attach_token_scope(token)
173 Session().commit()
174
175 h.flash(_("Auth token successfully created"), category='success')
176 return HTTPFound(h.route_path('my_account_auth_tokens'))
177
178 @LoginRequired()
179 @NotAnonymous()
180 @CSRFRequired()
181 @view_config(
182 route_name='my_account_auth_tokens_delete', request_method='POST')
183 def my_account_auth_tokens_delete(self):
184 _ = self.request.translate
185 c = self.load_default_context()
186
187 del_auth_token = self.request.POST.get('del_auth_token')
188
189 if del_auth_token:
190 AuthTokenModel().delete(del_auth_token, c.user.user_id)
191 Session().commit()
192 h.flash(_("Auth token successfully deleted"), category='success')
193
194 return HTTPFound(h.route_path('my_account_auth_tokens'))
@@ -0,0 +1,46 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 def includeme(config):
23
24 config.add_route(
25 name='repo_maintenance',
26 pattern='/{repo_name:.*?[^/]}/maintenance', repo_route=True)
27
28 config.add_route(
29 name='repo_maintenance_execute',
30 pattern='/{repo_name:.*?[^/]}/maintenance/execute', repo_route=True)
31
32
33 # Strip
34 config.add_route(
35 name='strip',
36 pattern='/{repo_name:.*?[^/]}/strip', repo_route=True)
37
38 config.add_route(
39 name='strip_check',
40 pattern='/{repo_name:.*?[^/]}/strip_check', repo_route=True)
41
42 config.add_route(
43 name='strip_execute',
44 pattern='/{repo_name:.*?[^/]}/strip_execute', repo_route=True)
45 # Scan module for configuration decorators.
46 config.scan()
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,70 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.view import view_config
24
25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
27 NotAnonymous)
28 from rhodecode.lib import repo_maintenance
29
30 log = logging.getLogger(__name__)
31
32
33 class RepoMaintenanceView(RepoAppView):
34 def load_default_context(self):
35 c = self._get_local_tmpl_context()
36
37 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
38 c.repo_info = self.db_repo
39
40 self._register_global_c(c)
41 return c
42
43 @LoginRequired()
44 @NotAnonymous()
45 @HasRepoPermissionAnyDecorator('repository.admin')
46 @view_config(
47 route_name='repo_maintenance', request_method='GET',
48 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
49 def repo_maintenance(self):
50 c = self.load_default_context()
51 c.active = 'maintenance'
52 maintenance = repo_maintenance.RepoMaintenance()
53 c.executable_tasks = maintenance.get_tasks_for_repo(self.db_repo)
54 return self._get_template_context(c)
55
56 @LoginRequired()
57 @NotAnonymous()
58 @HasRepoPermissionAnyDecorator('repository.admin')
59 @view_config(
60 route_name='repo_maintenance_execute', request_method='GET',
61 renderer='json', xhr=True)
62 def repo_maintenance_execute(self):
63 c = self.load_default_context()
64 c.active = 'maintenance'
65 _ = self.request.translate
66
67 maintenance = repo_maintenance.RepoMaintenance()
68 executed_types = maintenance.execute(self.db_repo)
69
70 return executed_types
@@ -0,0 +1,110 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 from pyramid.view import view_config
23
24 from rhodecode.apps._base import RepoAppView
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
26 NotAnonymous)
27
28
29 log = logging.getLogger(__name__)
30
31
32 class StripView(RepoAppView):
33 def load_default_context(self):
34 c = self._get_local_tmpl_context()
35
36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
37 c.repo_info = self.db_repo
38
39 self._register_global_c(c)
40 return c
41
42 @LoginRequired()
43 @NotAnonymous()
44 @HasRepoPermissionAnyDecorator('repository.admin')
45 @view_config(
46 route_name='strip', request_method='GET',
47 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
48 def strip(self):
49 c = self.load_default_context()
50 c.active = 'strip'
51 c.strip_limit = 10
52
53 return self._get_template_context(c)
54
55 @LoginRequired()
56 @NotAnonymous()
57 @HasRepoPermissionAnyDecorator('repository.admin')
58 @view_config(
59 route_name='strip_check', request_method='POST',
60 renderer='json', xhr=True
61 )
62 def strip_check(self):
63 from rhodecode.lib.vcs.backends.base import EmptyCommit
64 data = {}
65 rp = self.request.POST
66 for i in range(1, 11):
67 chset = 'changeset_id-%d'%(i,)
68 check = rp.get(chset)
69 if check:
70 data[i] = self.db_repo.get_changeset(rp[chset])
71 if isinstance(data[i], EmptyCommit):
72 data[i] = {'rev': None, 'commit': rp[chset]}
73 else:
74 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, 'author': data[i].author,
75 'comment': data[i].message}
76 else:
77 break
78 return data
79
80 @LoginRequired()
81 @NotAnonymous()
82 @HasRepoPermissionAnyDecorator('repository.admin')
83 @view_config(
84 route_name='strip_execute', request_method='POST',
85 renderer='json', xhr=True
86 )
87 def strip_execute(self):
88
89 from rhodecode.model.scm import ScmModel
90 from rhodecode.lib.ext_json import json
91
92 c = self.load_default_context()
93 user = self._rhodecode_user
94 rp = self.request.POST
95 data = {}
96 for idx in rp:
97 commit = json.loads(rp[idx])
98 #If someone put two times the same branch
99 if commit['branch'] in data.keys():
100 continue
101 try:
102 ScmModel().strip(repo=c.repo_info,
103 commit_id=commit['rev'], branch=commit['branch'])
104 log.info('Stripped commit %s from repo `%s` by %s' % (commit['rev'], c.repo_info.repo_name, user))
105 data[commit['rev']] = True
106 except Exception, e:
107 data[commit['rev']] = False
108 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (commit['rev'],
109 c.repo_info.repo_name, user, e.message))
110 return data
@@ -0,0 +1,28 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 def includeme(config):
23 config.add_route(
24 name='user_profile',
25 pattern='/_profiles/{username}')
26
27 # Scan module for configuration decorators.
28 config.scan()
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,75 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.model.db import User
24 from rhodecode.tests import (
25 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
26 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.utils import AssertResponse
29
30 fixture = Fixture()
31
32
33 def route_path(name, **kwargs):
34 return '/_profiles/{username}'.format(**kwargs)
35
36
37 class TestUsersController(TestController):
38
39 def test_user_profile(self, user_util):
40 edit_link_css = '.user-profile .panel-edit'
41 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
42 user = user_util.create_user(
43 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
44 username = user.username
45
46 response = self.app.get(route_path('user_profile', username=username))
47 response.mustcontain('testme')
48 response.mustcontain('testme@rhodecode.org')
49 assert_response = AssertResponse(response)
50 assert_response.no_element_exists(edit_link_css)
51
52 # edit should be available to superadmin users
53 self.logout_user()
54 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
55 response = self.app.get(route_path('user_profile', username=username))
56 assert_response = AssertResponse(response)
57 assert_response.element_contains(edit_link_css, 'Edit')
58
59 def test_user_profile_not_available(self, user_util):
60 user = user_util.create_user()
61 username = user.username
62
63 # not logged in, redirect
64 self.app.get(route_path('user_profile', username=username), status=302)
65
66 self.log_user()
67 # after log-in show
68 self.app.get(route_path('user_profile', username=username), status=200)
69
70 # default user, not allowed to show it
71 self.app.get(
72 route_path('user_profile', username=User.DEFAULT_USER), status=404)
73
74 # actual 404
75 self.app.get(route_path('user_profile', username='unknown'), status=404)
@@ -0,0 +1,53 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import HTTPNotFound
24 from pyramid.view import view_config
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.auth import LoginRequired, NotAnonymous
28
29 from rhodecode.model.db import User
30 from rhodecode.model.user import UserModel
31
32 log = logging.getLogger(__name__)
33
34
35 class UserProfileView(BaseAppView):
36
37 @LoginRequired()
38 @NotAnonymous()
39 @view_config(
40 route_name='user_profile', request_method='GET',
41 renderer='rhodecode:templates/users/user.mako')
42 def login(self):
43 # register local template context
44 c = self._get_local_tmpl_context()
45 c.active = 'user_profile'
46
47 username = self.request.matchdict.get('username')
48
49 c.user = UserModel().get_by_username(username)
50 if not c.user or c.user.username == User.DEFAULT_USER:
51 raise HTTPNotFound()
52
53 return self._get_template_context(c)
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,5 +1,5 b''
1 1 [bumpversion]
2 current_version = 4.6.1
2 current_version = 4.7.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
@@ -10,6 +10,8 b' include ='
10 10 omit =
11 11 rhodecode/lib/vcs/remote/*
12 12 rhodecode/lib/dbmigrate/*
13 rhodecode/lib/paster_commands/*
14 rhodecode/tests/*
13 15
14 16 [report]
15 17
@@ -4,26 +4,21 b' done = false'
4 4 [task:bump_version]
5 5 done = true
6 6
7 [task:rc_tools_pinned]
8 done = true
9
10 7 [task:fixes_on_stable]
11 done = true
12 8
13 9 [task:pip2nix_generated]
14 done = true
15 10
16 11 [task:changelog_updated]
17 done = true
18 12
19 13 [task:generate_api_docs]
20 done = true
14
15 [task:updated_translation]
21 16
22 17 [release]
23 state = prepared
24 version = 4.6.1
18 state = in_progress
19 version = 4.7.0
25 20
26 [task:updated_translation]
21 [task:rc_tools_pinned]
27 22
28 23 [task:generate_js_routes]
29 24
@@ -1,5 +1,5 b''
1 1 # top level files
2 include test.ini
2
3 3 include MANIFEST.in
4 4 include README.rst
5 5 include CHANGES.rst
@@ -539,18 +539,15 b' vcs.server = localhost:9900'
539 539
540 540 ## Web server connectivity protocol, responsible for web based VCS operatations
541 541 ## Available protocols are:
542 ## `pyro4` - use pyro4 server
543 542 ## `http` - use http-rpc backend (default)
544 543 vcs.server.protocol = http
545 544
546 545 ## Push/Pull operations protocol, available options are:
547 ## `pyro4` - use pyro4 server
548 546 ## `http` - use http-rpc backend (default)
549 547 ##
550 548 vcs.scm_app_implementation = http
551 549
552 550 ## Push/Pull operations hooks protocol, available options are:
553 ## `pyro4` - use pyro4 server
554 551 ## `http` - use http-rpc backend (default)
555 552 vcs.hooks.protocol = http
556 553
@@ -666,7 +663,7 b' formatter = color_formatter_sql'
666 663 ################
667 664
668 665 [formatter_generic]
669 class = rhodecode.lib.logging_formatter.Pyro4AwareFormatter
666 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
670 667 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
671 668 datefmt = %Y-%m-%d %H:%M:%S
672 669
@@ -36,7 +36,7 b' tmp_upload_dir = None'
36 36
37 37 # Custom log format
38 38 access_log_format = (
39 '%(t)s GNCRN %(p)-8s %(h)-15s rqt:%(L)s %(s)s %(b)s "%(m)s:%(U)s %(q)s" usr:%(u)s "%(f)s" "%(a)s"')
39 '%(t)s GNCRN %(p)-8s %(h)-15s rqt:%(L)s %(s)s %(b)-6s "%(m)s:%(U)s %(q)s" usr:%(u)s "%(f)s" "%(a)s"')
40 40
41 41 # self adjust workers based on CPU count
42 42 # workers = multiprocessing.cpu_count() * 2 + 1
@@ -508,18 +508,15 b' vcs.server = localhost:9900'
508 508
509 509 ## Web server connectivity protocol, responsible for web based VCS operatations
510 510 ## Available protocols are:
511 ## `pyro4` - use pyro4 server
512 511 ## `http` - use http-rpc backend (default)
513 512 vcs.server.protocol = http
514 513
515 514 ## Push/Pull operations protocol, available options are:
516 ## `pyro4` - use pyro4 server
517 515 ## `http` - use http-rpc backend (default)
518 516 ##
519 517 vcs.scm_app_implementation = http
520 518
521 519 ## Push/Pull operations hooks protocol, available options are:
522 ## `pyro4` - use pyro4 server
523 520 ## `http` - use http-rpc backend (default)
524 521 vcs.hooks.protocol = http
525 522
@@ -635,7 +632,7 b' formatter = generic'
635 632 ################
636 633
637 634 [formatter_generic]
638 class = rhodecode.lib.logging_formatter.Pyro4AwareFormatter
635 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
639 636 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
640 637 datefmt = %Y-%m-%d %H:%M:%S
641 638
@@ -124,6 +124,7 b' let'
124 124 name = "rhodecode-enterprise-ce-${version}";
125 125 releaseName = "RhodeCodeEnterpriseCE-${version}";
126 126 src = rhodecode-enterprise-ce-src;
127 dontStrip = true; # prevent strip, we don't need it.
127 128
128 129 buildInputs =
129 130 attrs.buildInputs ++
@@ -225,8 +226,8 b' let'
225 226 rhodecode-testdata-src = sources.rhodecode-testdata or (
226 227 pkgs.fetchhg {
227 228 url = "https://code.rhodecode.com/upstream/rc_testdata";
228 rev = "v0.9.0";
229 sha256 = "0k0ccb7cncd6mmzwckfbr6l7fsymcympwcm948qc3i0f0m6bbg1y";
229 rev = "v0.10.0";
230 sha256 = "0zn9swwvx4vgw4qn8q3ri26vvzgrxn15x6xnjrysi1bwmz01qjl0";
230 231 });
231 232
232 233 # Apply all overrides and fix the final package set
@@ -17,7 +17,6 b' implemented in :mod:`rhodecode.lib.middl'
17 17 .. toctree::
18 18 :maxdepth: 2
19 19
20 http-transition
21 20 middleware
22 21 vcsserver
23 22 subversion
@@ -59,7 +59,7 b' Below config if for an Apache Reverse Pr'
59 59
60 60 # Url to running RhodeCode instance. This is shown as `- URL:` when
61 61 # running rccontrol status.
62 ProxyPass / http://127.0.0.1:10002/
62 ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
63 63 ProxyPassReverse / http://127.0.0.1:10002/
64 64
65 65 # strict http prevents from https -> http downgrade
@@ -47,7 +47,7 b' the ``debug`` level.'
47 47 ### LOGGING CONFIGURATION ####
48 48 ################################
49 49 [loggers]
50 keys = root, routes, rhodecode, sqlalchemy, beaker, pyro4, templates
50 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
51 51
52 52 [handlers]
53 53 keys = console, console_sql, file, file_rotating
@@ -75,12 +75,6 b' the ``debug`` level.'
75 75 qualname = beaker.container
76 76 propagate = 1
77 77
78 [logger_pyro4]
79 level = DEBUG
80 handlers =
81 qualname = Pyro4
82 propagate = 1
83
84 78 [logger_templates]
85 79 level = INFO
86 80 handlers =
@@ -114,8 +114,20 b' 4. Add the following configuration optio'
114 114
115 115 This would create a special template file called ```mod_dav_svn.conf```. We
116 116 used that file path in the apache config above inside the Include statement.
117 It's also possible to generate the config from the
118 :menuselection:`Admin --> Settings --> VCS` page.
117 It's also possible to manually generate the config from the
118 :menuselection:`Admin --> Settings --> VCS` page by clicking a
119 `Generate Apache Config` button.
120
121 5. Now only things left is to enable svn support, and generate the initial
122 configuration.
123
124 - Select `Proxy subversion HTTP requests` checkbox
125 - Enter http://localhost:8090 into `Subversion HTTP Server URL`
126 - Click the `Generate Apache Config` button.
127
128 This config will be automatically re-generated once an user-groups is added
129 to properly map the additional paths generated.
130
119 131
120 132
121 133 Using |svn|
@@ -7,26 +7,8 b' Change the |hg| Large Files Location'
7 7 :file:`/home/{user}/repos/.cache/largefiles`. If you wish to change this, use
8 8 the following steps:
9 9
10 1. Open ishell from the terminal and use it to log into the |RCE| database by
11 specifying the instance :file:`rhodecode.ini` file.
12
13 .. code-block:: bash
14
15 # Open iShell from the terminal and set ini file
16 $ rccontrol ishell enterprise-1
17
18 2. Run the following commands, and ensure that |RCE| has write access to the
19 new directory:
10 1. Open :menuselection:`Admin --> Settings --> VCS` as super-admin.
20 11
21 .. code-block:: bash
12 In section called `Mercurial Settings` you can change where the largefiles
13 objects should be stored.
22 14
23 # Once logged into the database, use SQL to redirect
24 # the large files location
25 In [1]: from rhodecode.model.settings import SettingsModel
26 In [2]: SettingsModel().get_ui_by_key('usercache')
27 Out[2]: <RhodeCodeUi[largefiles]usercache=>/mnt/hgfs/shared/workspace/xxxx/.cache/largefiles]>
28
29 In [3]: largefiles_cache = SettingsModel().get_ui_by_key('usercache')
30 In [4]: largefiles_cache.ui_value = '/new/path’
31 In [5]: Session().add(largefiles_cache);Session().commit()
32
@@ -17,5 +17,6 b' may find some of the following methods u'
17 17 tuning-mount-cache-memory
18 18 tuning-change-encoding
19 19 tuning-change-large-file-dir
20 tuning-change-lfs-dir
20 21 tuning-hg-auth-loop
21 22
@@ -232,7 +232,7 b' For a more detailed explanation of the l'
232 232 ### LOGGING CONFIGURATION ####
233 233 ################################
234 234 [loggers]
235 keys = root, vcsserver, pyro4, beaker
235 keys = root, vcsserver, beaker
236 236
237 237 [handlers]
238 238 keys = console
@@ -259,12 +259,6 b' For a more detailed explanation of the l'
259 259 qualname = beaker
260 260 propagate = 1
261 261
262 [logger_pyro4]
263 level = DEBUG
264 handlers =
265 qualname = Pyro4
266 propagate = 1
267
268 262
269 263 ##############
270 264 ## HANDLERS ##
@@ -75,7 +75,7 b' Example call for auto pulling from remot'
75 75 .. code-block:: bash
76 76
77 77 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,
78 "auth_token":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull", "args":{"repo":"CPython"}}'
78 "auth_token":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull", "args":{"repoid":"CPython"}}'
79 79
80 80 Provide those parameters:
81 81 - **id** A value of any type, which is used to match the response with the
@@ -24,13 +24,12 b' close_pull_request'
24 24
25 25 .. code-block:: bash
26 26
27 "id": <id_given_in_input>,
28 "result":
29 {
27 "id": <id_given_in_input>,
28 "result": {
30 29 "pull_request_id": "<int>",
31 30 "closed": "<bool>"
32 31 },
33 "error": null
32 "error": null
34 33
35 34
36 35 comment_pull_request
@@ -67,15 +66,14 b' comment_pull_request'
67 66
68 67 .. code-block:: bash
69 68
70 id : <id_given_in_input>
71 result :
72 {
69 id : <id_given_in_input>
70 result : {
73 71 "pull_request_id": "<Integer>",
74 72 "comment_id": "<Integer>",
75 73 "status": {"given": <given_status>,
76 74 "was_changed": <bool status_was_actually_changed> },
77 }
78 error : null
75 },
76 error : null
79 77
80 78
81 79 create_pull_request
@@ -109,9 +107,8 b' create_pull_request'
109 107 :param reviewers: Set the new pull request reviewers list.
110 108 :type reviewers: Optional(list)
111 109 Accepts username strings or objects of the format:
112 {
113 'username': 'nick', 'reasons': ['original author']
114 }
110
111 {'username': 'nick', 'reasons': ['original author']}
115 112
116 113
117 114 get_pull_request
@@ -305,9 +302,8 b' merge_pull_request'
305 302
306 303 .. code-block:: bash
307 304
308 "id": <id_given_in_input>,
309 "result":
310 {
305 "id": <id_given_in_input>,
306 "result": {
311 307 "executed": "<bool>",
312 308 "failure_reason": "<int>",
313 309 "merge_commit_id": "<merge_commit_id>",
@@ -318,7 +314,7 b' merge_pull_request'
318 314 "name": "<name>"
319 315 }
320 316 },
321 "error": null
317 "error": null
322 318
323 319
324 320 update_pull_request
@@ -349,9 +345,8 b' update_pull_request'
349 345
350 346 .. code-block:: bash
351 347
352 id : <id_given_in_input>
353 result :
354 {
348 id : <id_given_in_input>
349 result : {
355 350 "msg": "Updated pull request `63`",
356 351 "pull_request": <pull_request_object>,
357 352 "updated_reviewers": {
@@ -371,6 +366,6 b' update_pull_request'
371 366 "removed": []
372 367 }
373 368 }
374 error : null
369 error : null
375 370
376 371
@@ -85,6 +85,58 b' get_ip'
85 85 }
86 86
87 87
88 get_method
89 ----------
90
91 .. py:function:: get_method(apiuser, pattern=<Optional:'*'>)
92
93 Returns list of all available API methods. By default match pattern
94 os "*" but any other pattern can be specified. eg *comment* will return
95 all methods with comment inside them. If just single method is matched
96 returned data will also include method specification
97
98 This command can only be run using an |authtoken| with admin rights to
99 the specified repository.
100
101 This command takes the following options:
102
103 :param apiuser: This is filled automatically from the |authtoken|.
104 :type apiuser: AuthUser
105 :param pattern: pattern to match method names against
106 :type older_then: Optional("*")
107
108 Example output:
109
110 .. code-block:: bash
111
112 id : <id_given_in_input>
113 "result": [
114 "changeset_comment",
115 "comment_pull_request",
116 "comment_commit"
117 ]
118 error : null
119
120 .. code-block:: bash
121
122 id : <id_given_in_input>
123 "result": [
124 "comment_commit",
125 {
126 "apiuser": "<RequiredType>",
127 "comment_type": "<Optional:u'note'>",
128 "commit_id": "<RequiredType>",
129 "message": "<RequiredType>",
130 "repoid": "<RequiredType>",
131 "request": "<RequiredType>",
132 "resolves_comment_id": "<Optional:None>",
133 "status": "<Optional:None>",
134 "userid": "<Optional:<OptionalAttr:apiuser>>"
135 }
136 ]
137 error : null
138
139
88 140 get_server_info
89 141 ---------------
90 142
@@ -41,15 +41,16 b' create_user'
41 41 :type force_password_change: Optional(``True`` | ``False``)
42 42 :param create_personal_repo_group: Create personal repo group for this user
43 43 :type create_personal_repo_group: Optional(``True`` | ``False``)
44
44 45 Example output:
45 46
46 47 .. code-block:: bash
47 48
48 49 id : <id_given_in_input>
49 50 result: {
50 "msg" : "created new user `<username>`",
51 "user": <user_obj>
52 }
51 "msg" : "created new user `<username>`",
52 "user": <user_obj>
53 }
53 54 error: null
54 55
55 56 Example error output:
@@ -98,9 +99,9 b' delete_user'
98 99
99 100 id : <id_given_in_input>
100 101 result: {
101 "msg" : "deleted user ID:<userid> <username>",
102 "user": null
103 }
102 "msg" : "deleted user ID:<userid> <username>",
103 "user": null
104 }
104 105 error: null
105 106
106 107 Example error output:
@@ -145,8 +146,8 b' get_user'
145 146 "result": {
146 147 "active": true,
147 148 "admin": false,
148 "api_key": "api-key",
149 149 "api_keys": [ list of keys ],
150 "auth_tokens": [ list of tokens with details ],
150 151 "email": "user@example.com",
151 152 "emails": [
152 153 "user@example.com"
@@ -157,6 +158,7 b' get_user'
157 158 "ip_addresses": [],
158 159 "language": null,
159 160 "last_login": "Timestamp",
161 "last_activity": "Timestamp",
160 162 "lastname": "surnae",
161 163 "permissions": {
162 164 "global": [
@@ -183,6 +185,32 b' get_user'
183 185 }
184 186
185 187
188 get_user_audit_logs
189 -------------------
190
191 .. py:function:: get_user_audit_logs(apiuser, userid=<Optional:<OptionalAttr:apiuser>>)
192
193 Fetches all action logs made by the specified user.
194
195 This command takes the following options:
196
197 :param apiuser: This is filled automatically from the |authtoken|.
198 :type apiuser: AuthUser
199 :param userid: Sets the userid whose list of locked |repos| will be
200 displayed.
201 :type userid: Optional(str or int)
202
203 Example output:
204
205 .. code-block:: bash
206
207 id : <id_given_in_input>
208 result : {
209 [action, action,...]
210 }
211 error : null
212
213
186 214 get_user_locks
187 215 --------------
188 216
@@ -232,7 +260,7 b' get_users'
232 260 .. code-block:: bash
233 261
234 262 id : <id_given_in_input>
235 result: [<user_object>, ...]
263 result: [<user_object>, ...]
236 264 error: null
237 265
238 266
@@ -279,9 +307,9 b' update_user'
279 307
280 308 id : <id_given_in_input>
281 309 result: {
282 "msg" : "updated user ID:<userid> <username>",
283 "user": <user_object>,
284 }
310 "msg" : "updated user ID:<userid> <username>",
311 "user": <user_object>,
312 }
285 313 error: null
286 314
287 315 Example error output:
@@ -19,4 +19,11 b' authentication.'
19 19 # Set the Active Directory user surname
20 20 Last Name Attribute = user_surname
21 21 # Set the Active Directory user email
22 E-mail Attribute = userEmail No newline at end of file
22 E-mail Attribute = userEmail
23
24
25 Below is example setup that can be used with Active Directory and ldap groups.
26
27 .. image:: ../images/ldap-groups-example.png
28 :alt: LDAP/AD setup example
29 :scale: 50 % No newline at end of file
@@ -104,4 +104,10 b' The following are optional when enabling'
104 104 following directory: `/etc/openldap/cacerts`
105 105
106 106
107 Below is example setup that can be used with Active Directory and ldap groups.
108
109 .. image:: ../images/ldap-groups-example.png
110 :alt: LDAP/AD setup example
111 :scale: 50 %
112
107 113 .. _RFC 2254: http://www.rfc-base.org/rfc-2254.html No newline at end of file
@@ -114,6 +114,30 b' following command from inside the cloned'
114 114 fine on both MacOS and Linux platforms.
115 115
116 116
117 Create config.nix for development
118 ---------------------------------
119
120 In order to run proper tests and setup linking across projects, a config.nix
121 file needs to be setup::
122
123 # create config
124 mkdir -p ~/.nixpkgs
125 touch ~/.nixpkgs/config.nix
126
127 # put the below content into the ~/.nixpkgs/config.nix file
128 # adjusts, the path to where you cloned your repositories.
129
130 {
131 rc = {
132 sources = {
133 rhodecode-vcsserver = "/home/dev/rhodecode-vcsserver";
134 rhodecode-enterprise-ce = "/home/dev/rhodecode-enterprise-ce";
135 rhodecode-enterprise-ee = "/home/dev/rhodecode-enterprise-ee";
136 };
137 };
138 }
139
140
117 141
118 142 Creating a Development Configuration
119 143 ------------------------------------
@@ -21,7 +21,7 b' New Features'
21 21 - Pull request reviewers (EE only): added new default reviewers functionality.
22 22 Allows picking users or user groups defined as reviewers for new pull request.
23 23 Picking reviewers can be based on branch name, changed file name patterns or
24 original author of changed source code. eg *.css -> design team.
24 original author of changed source code. eg \*.css -> design team.
25 25 Master branch -> repo owner, fixes #1131.
26 26 - Pull request reviewers: store and show reasons why given person is a reviewer.
27 27 Manually adding reviewers after creating a PR will now be also indicated
@@ -85,7 +85,7 b' General'
85 85 input for text box.
86 86 - Api: WARNING DEPRECATION, refactor repository group schemas. Fixes #4133.
87 87 When using create_repo, create_repo_group, update_repo, update_repo_group
88 the *_name parameter now takes full path including sub repository groups.
88 the \*_name parameter now takes full path including sub repository groups.
89 89 This is the only way to add resource under another repository group.
90 90 Furthermore giving non-existing path will no longer create the missing
91 91 structure. This change makes the api more consistent, it better validates
@@ -9,6 +9,7 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.7.0.rst
12 13 release-notes-4.6.1.rst
13 14 release-notes-4.6.0.rst
14 15 release-notes-4.5.2.rst
@@ -9,6 +9,36 b' has a large files extension which tracks'
9 9 means that the large files are only downloaded when they are needed as part
10 10 of the current revision. This saves both disk space and bandwidth.
11 11
12
13 Enabling HG Largefiles
14 ++++++++++++++++++++++
15
16 Mercurial Largefiles extension is disabled by default within |RC| Server.
17
18 To enable Mercurial Largefiles Globally:
19
20 - Go to :menuselection:`Admin --> Settings --> VCS`
21
22 - Scroll down into `Mercurial settings`
23
24 - Tick `Enable largefiles extension`
25
26 - Save your settings.
27
28 Those settings apply globally to each repository that inherits from the defaults
29 You can leave `largefiles extension` disabled globally, and only enable it per
30 repository that would use the largefiles.
31
32
33 .. note::
34
35 You might want to adjust the global storage location at that point, however
36 we recommend leaving the default one created.
37
38
39 Installing and using the |hg| Largefiles
40 ++++++++++++++++++++++++++++++++++++++++
41
12 42 To find out more, see the |hg| `Large Files Extensions Documentation`_.
13 43
14 44 To configure the large files extension, you need to set up your
@@ -12,6 +12,7 b' then please send a request to support@rh'
12 12
13 13 deploy-from-host
14 14 hg-large-ext
15 git-lfs-ext
15 16 multi-instance-setup
16 17 scaling-best-practices
17 18 squash-commits
@@ -48,12 +48,27 b' self: super: {'
48 48 ];
49 49 });
50 50
51 nbconvert = super.nbconvert.override (attrs: {
52 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
53 # marcink: plug in jupyter-client for notebook rendering
54 self.jupyter-client
55 ];
56 });
57
51 58 ipython = super.ipython.override (attrs: {
52 59 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
53 60 self.gnureadline
54 61 ];
55 62 });
56 63
64 celery = super.celery.override (attrs: {
65 # The current version of kombu needs some patching to work with the
66 # other libs. Should be removed once we update celery and kombu.
67 patches = [
68 ./patch-celery-dateutil.diff
69 ];
70 });
71
57 72 kombu = super.kombu.override (attrs: {
58 73 # The current version of kombu needs some patching to work with the
59 74 # other libs. Should be removed once we update celery and kombu.
@@ -197,19 +197,6 b''
197 197 license = [ pkgs.lib.licenses.bsdOriginal ];
198 198 };
199 199 };
200 Pyro4 = super.buildPythonPackage {
201 name = "Pyro4-4.41";
202 buildInputs = with self; [];
203 doCheck = false;
204 propagatedBuildInputs = with self; [serpent];
205 src = fetchurl {
206 url = "https://pypi.python.org/packages/56/2b/89b566b4bf3e7f8ba790db2d1223852f8cb454c52cab7693dd41f608ca2a/Pyro4-4.41.tar.gz";
207 md5 = "ed69e9bfafa9c06c049a87cb0c4c2b6c";
208 };
209 meta = {
210 license = [ pkgs.lib.licenses.mit ];
211 };
212 };
213 200 Routes = super.buildPythonPackage {
214 201 name = "Routes-1.13";
215 202 buildInputs = with self; [];
@@ -444,6 +431,19 b''
444 431 license = [ pkgs.lib.licenses.mit ];
445 432 };
446 433 };
434 bleach = super.buildPythonPackage {
435 name = "bleach-1.5.0";
436 buildInputs = with self; [];
437 doCheck = false;
438 propagatedBuildInputs = with self; [six html5lib];
439 src = fetchurl {
440 url = "https://pypi.python.org/packages/99/00/25a8fce4de102bf6e3cc76bc4ea60685b2fee33bde1b34830c70cacc26a7/bleach-1.5.0.tar.gz";
441 md5 = "b663300efdf421b3b727b19d7be9c7e7";
442 };
443 meta = {
444 license = [ pkgs.lib.licenses.asl20 ];
445 };
446 };
447 447 bottle = super.buildPythonPackage {
448 448 name = "bottle-0.12.8";
449 449 buildInputs = with self; [];
@@ -535,6 +535,19 b''
535 535 license = [ pkgs.lib.licenses.bsdOriginal ];
536 536 };
537 537 };
538 configparser = super.buildPythonPackage {
539 name = "configparser-3.5.0";
540 buildInputs = with self; [];
541 doCheck = false;
542 propagatedBuildInputs = with self; [];
543 src = fetchurl {
544 url = "https://pypi.python.org/packages/7c/69/c2ce7e91c89dc073eb1aa74c0621c3eefbffe8216b3f9af9d3885265c01c/configparser-3.5.0.tar.gz";
545 md5 = "cfdd915a5b7a6c09917a64a573140538";
546 };
547 meta = {
548 license = [ pkgs.lib.licenses.mit ];
549 };
550 };
538 551 cov-core = super.buildPythonPackage {
539 552 name = "cov-core-1.15.0";
540 553 buildInputs = with self; [];
@@ -562,29 +575,29 b''
562 575 };
563 576 };
564 577 cssselect = super.buildPythonPackage {
565 name = "cssselect-0.9.1";
578 name = "cssselect-1.0.1";
566 579 buildInputs = with self; [];
567 580 doCheck = false;
568 581 propagatedBuildInputs = with self; [];
569 582 src = fetchurl {
570 url = "https://pypi.python.org/packages/aa/e5/9ee1460d485b94a6d55732eb7ad5b6c084caf73dd6f9cb0bb7d2a78fafe8/cssselect-0.9.1.tar.gz";
571 md5 = "c74f45966277dc7a0f768b9b0f3522ac";
583 url = "https://pypi.python.org/packages/77/ff/9c865275cd19290feba56344eba570e719efb7ca5b34d67ed12b22ebbb0d/cssselect-1.0.1.tar.gz";
584 md5 = "3fa03bf82a9f0b1223c0f1eb1369e139";
572 585 };
573 586 meta = {
574 587 license = [ pkgs.lib.licenses.bsdOriginal ];
575 588 };
576 589 };
577 590 decorator = super.buildPythonPackage {
578 name = "decorator-3.4.2";
591 name = "decorator-4.0.11";
579 592 buildInputs = with self; [];
580 593 doCheck = false;
581 594 propagatedBuildInputs = with self; [];
582 595 src = fetchurl {
583 url = "https://pypi.python.org/packages/35/3a/42566eb7a2cbac774399871af04e11d7ae3fc2579e7dae85213b8d1d1c57/decorator-3.4.2.tar.gz";
584 md5 = "9e0536870d2b83ae27d58dbf22582f4d";
596 url = "https://pypi.python.org/packages/cc/ac/5a16f1fc0506ff72fcc8fd4e858e3a1c231f224ab79bb7c4c9b2094cc570/decorator-4.0.11.tar.gz";
597 md5 = "73644c8f0bd4983d1b6a34b49adec0ae";
585 598 };
586 599 meta = {
587 license = [ pkgs.lib.licenses.bsdOriginal ];
600 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "new BSD License"; } ];
588 601 };
589 602 };
590 603 deform = super.buildPythonPackage {
@@ -678,6 +691,19 b''
678 691 license = [ pkgs.lib.licenses.asl20 ];
679 692 };
680 693 };
694 entrypoints = super.buildPythonPackage {
695 name = "entrypoints-0.2.2";
696 buildInputs = with self; [];
697 doCheck = false;
698 propagatedBuildInputs = with self; [configparser];
699 src = fetchurl {
700 url = "https://code.rhodecode.com/upstream/entrypoints/archive/96e6d645684e1af3d7df5b5272f3fe85a546b233.tar.gz?md5=7db37771aea9ac9fefe093e5d6987313";
701 md5 = "7db37771aea9ac9fefe093e5d6987313";
702 };
703 meta = {
704 license = [ pkgs.lib.licenses.mit ];
705 };
706 };
681 707 enum34 = super.buildPythonPackage {
682 708 name = "enum34-1.1.6";
683 709 buildInputs = with self; [];
@@ -691,6 +717,19 b''
691 717 license = [ pkgs.lib.licenses.bsdOriginal ];
692 718 };
693 719 };
720 functools32 = super.buildPythonPackage {
721 name = "functools32-3.2.3.post2";
722 buildInputs = with self; [];
723 doCheck = false;
724 propagatedBuildInputs = with self; [];
725 src = fetchurl {
726 url = "https://pypi.python.org/packages/5e/1a/0aa2c8195a204a9f51284018562dea77e25511f02fe924fac202fc012172/functools32-3.2.3-2.zip";
727 md5 = "d55232eb132ec779e6893c902a0bc5ad";
728 };
729 meta = {
730 license = [ pkgs.lib.licenses.psfl ];
731 };
732 };
694 733 future = super.buildPythonPackage {
695 734 name = "future-0.14.3";
696 735 buildInputs = with self; [];
@@ -782,6 +821,19 b''
782 821 license = [ pkgs.lib.licenses.mit ];
783 822 };
784 823 };
824 html5lib = super.buildPythonPackage {
825 name = "html5lib-0.9999999";
826 buildInputs = with self; [];
827 doCheck = false;
828 propagatedBuildInputs = with self; [six];
829 src = fetchurl {
830 url = "https://pypi.python.org/packages/ae/ae/bcb60402c60932b32dfaf19bb53870b29eda2cd17551ba5639219fb5ebf9/html5lib-0.9999999.tar.gz";
831 md5 = "ef43cb05e9e799f25d65d1135838a96f";
832 };
833 meta = {
834 license = [ pkgs.lib.licenses.mit ];
835 };
836 };
785 837 infrae.cache = super.buildPythonPackage {
786 838 name = "infrae.cache-1.0.1";
787 839 buildInputs = with self; [];
@@ -873,6 +925,45 b''
873 925 license = [ pkgs.lib.licenses.bsdOriginal ];
874 926 };
875 927 };
928 jsonschema = super.buildPythonPackage {
929 name = "jsonschema-2.6.0";
930 buildInputs = with self; [];
931 doCheck = false;
932 propagatedBuildInputs = with self; [functools32];
933 src = fetchurl {
934 url = "https://pypi.python.org/packages/58/b9/171dbb07e18c6346090a37f03c7e74410a1a56123f847efed59af260a298/jsonschema-2.6.0.tar.gz";
935 md5 = "50c6b69a373a8b55ff1e0ec6e78f13f4";
936 };
937 meta = {
938 license = [ pkgs.lib.licenses.mit ];
939 };
940 };
941 jupyter-client = super.buildPythonPackage {
942 name = "jupyter-client-5.0.0";
943 buildInputs = with self; [];
944 doCheck = false;
945 propagatedBuildInputs = with self; [traitlets jupyter-core pyzmq python-dateutil];
946 src = fetchurl {
947 url = "https://pypi.python.org/packages/e5/6f/65412ed462202b90134b7e761b0b7e7f949e07a549c1755475333727b3d0/jupyter_client-5.0.0.tar.gz";
948 md5 = "1acd331b5c9fb4d79dae9939e79f2426";
949 };
950 meta = {
951 license = [ pkgs.lib.licenses.bsdOriginal ];
952 };
953 };
954 jupyter-core = super.buildPythonPackage {
955 name = "jupyter-core-4.3.0";
956 buildInputs = with self; [];
957 doCheck = false;
958 propagatedBuildInputs = with self; [traitlets];
959 src = fetchurl {
960 url = "https://pypi.python.org/packages/2f/39/5138f975100ce14d150938df48a83cd852a3fd8e24b1244f4113848e69e2/jupyter_core-4.3.0.tar.gz";
961 md5 = "18819511a809afdeed9a995a9c27bcfb";
962 };
963 meta = {
964 license = [ pkgs.lib.licenses.bsdOriginal ];
965 };
966 };
876 967 kombu = super.buildPythonPackage {
877 968 name = "kombu-1.5.1";
878 969 buildInputs = with self; [];
@@ -887,13 +978,13 b''
887 978 };
888 979 };
889 980 lxml = super.buildPythonPackage {
890 name = "lxml-3.4.4";
981 name = "lxml-3.7.3";
891 982 buildInputs = with self; [];
892 983 doCheck = false;
893 984 propagatedBuildInputs = with self; [];
894 985 src = fetchurl {
895 url = "https://pypi.python.org/packages/63/c7/4f2a2a4ad6c6fa99b14be6b3c1cece9142e2d915aa7c43c908677afc8fa4/lxml-3.4.4.tar.gz";
896 md5 = "a9a65972afc173ec7a39c585f4eea69c";
986 url = "https://pypi.python.org/packages/39/e8/a8e0b1fa65dd021d48fe21464f71783655f39a41f218293c1c590d54eb82/lxml-3.7.3.tar.gz";
987 md5 = "075692ce442e69bbd604d44e21c02753";
897 988 };
898 989 meta = {
899 990 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -912,6 +1003,19 b''
912 1003 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
913 1004 };
914 1005 };
1006 mistune = super.buildPythonPackage {
1007 name = "mistune-0.7.3";
1008 buildInputs = with self; [];
1009 doCheck = false;
1010 propagatedBuildInputs = with self; [];
1011 src = fetchurl {
1012 url = "https://pypi.python.org/packages/88/1e/be99791262b3a794332fda598a07c2749a433b9378586361ba9d8e824607/mistune-0.7.3.tar.gz";
1013 md5 = "4eba50bd121b83716fa4be6a4049004b";
1014 };
1015 meta = {
1016 license = [ pkgs.lib.licenses.bsdOriginal ];
1017 };
1018 };
915 1019 mock = super.buildPythonPackage {
916 1020 name = "mock-1.0.1";
917 1021 buildInputs = with self; [];
@@ -938,6 +1042,32 b''
938 1042 license = [ pkgs.lib.licenses.asl20 ];
939 1043 };
940 1044 };
1045 nbconvert = super.buildPythonPackage {
1046 name = "nbconvert-5.1.1";
1047 buildInputs = with self; [];
1048 doCheck = false;
1049 propagatedBuildInputs = with self; [mistune Jinja2 Pygments traitlets jupyter-core nbformat entrypoints bleach pandocfilters testpath];
1050 src = fetchurl {
1051 url = "https://pypi.python.org/packages/95/58/df1c91f1658ee5df19097f915a1e71c91fc824a708d82d2b2e35f8b80e9a/nbconvert-5.1.1.tar.gz";
1052 md5 = "d0263fb03a44db2f94eea09a608ed813";
1053 };
1054 meta = {
1055 license = [ pkgs.lib.licenses.bsdOriginal ];
1056 };
1057 };
1058 nbformat = super.buildPythonPackage {
1059 name = "nbformat-4.3.0";
1060 buildInputs = with self; [];
1061 doCheck = false;
1062 propagatedBuildInputs = with self; [ipython-genutils traitlets jsonschema jupyter-core];
1063 src = fetchurl {
1064 url = "https://pypi.python.org/packages/f9/c5/89df4abf906f766727f976e170caa85b4f1c1d1feb1f45d716016e68e19f/nbformat-4.3.0.tar.gz";
1065 md5 = "9a00d20425914cd5ba5f97769d9963ca";
1066 };
1067 meta = {
1068 license = [ pkgs.lib.licenses.bsdOriginal ];
1069 };
1070 };
941 1071 nose = super.buildPythonPackage {
942 1072 name = "nose-1.3.6";
943 1073 buildInputs = with self; [];
@@ -977,6 +1107,19 b''
977 1107 license = [ pkgs.lib.licenses.asl20 ];
978 1108 };
979 1109 };
1110 pandocfilters = super.buildPythonPackage {
1111 name = "pandocfilters-1.4.1";
1112 buildInputs = with self; [];
1113 doCheck = false;
1114 propagatedBuildInputs = with self; [];
1115 src = fetchurl {
1116 url = "https://pypi.python.org/packages/e3/1f/21d1b7e8ca571e80b796c758d361fdf5554335ff138158654684bc5401d8/pandocfilters-1.4.1.tar.gz";
1117 md5 = "7680d9f9ec07397dd17f380ee3818b9d";
1118 };
1119 meta = {
1120 license = [ pkgs.lib.licenses.bsdOriginal ];
1121 };
1122 };
980 1123 paramiko = super.buildPythonPackage {
981 1124 name = "paramiko-1.15.1";
982 1125 buildInputs = with self; [];
@@ -1043,13 +1186,13 b''
1043 1186 };
1044 1187 };
1045 1188 prompt-toolkit = super.buildPythonPackage {
1046 name = "prompt-toolkit-1.0.9";
1189 name = "prompt-toolkit-1.0.13";
1047 1190 buildInputs = with self; [];
1048 1191 doCheck = false;
1049 1192 propagatedBuildInputs = with self; [six wcwidth];
1050 1193 src = fetchurl {
1051 url = "https://pypi.python.org/packages/83/14/5ac258da6c530eca02852ee25c7a9ff3ca78287bb4c198d0d0055845d856/prompt_toolkit-1.0.9.tar.gz";
1052 md5 = "a39f91a54308fb7446b1a421c11f227c";
1194 url = "https://pypi.python.org/packages/23/be/4876b52d5cc159cbd4b0ff6e7aa419a26470849a43a8f647857a4a24467b/prompt_toolkit-1.0.13.tar.gz";
1195 md5 = "427b496d2c147bd3819bc3a7f6e0d493";
1053 1196 };
1054 1197 meta = {
1055 1198 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1199,13 +1342,13 b''
1199 1342 };
1200 1343 };
1201 1344 pyramid = super.buildPythonPackage {
1202 name = "pyramid-1.6.1";
1345 name = "pyramid-1.7.4";
1203 1346 buildInputs = with self; [];
1204 1347 doCheck = false;
1205 1348 propagatedBuildInputs = with self; [setuptools WebOb repoze.lru zope.interface zope.deprecation venusian translationstring PasteDeploy];
1206 1349 src = fetchurl {
1207 url = "https://pypi.python.org/packages/30/b3/fcc4a2a4800cbf21989e00454b5828cf1f7fe35c63e0810b350e56d4c475/pyramid-1.6.1.tar.gz";
1208 md5 = "b18688ff3cc33efdbb098a35b45dd122";
1350 url = "https://pypi.python.org/packages/33/91/55f5c661f8923902cd1f68d75f2b937c45e7682857356cf18f0be5493899/pyramid-1.7.4.tar.gz";
1351 md5 = "6ef1dfdcff9136d04490410757c4c446";
1209 1352 };
1210 1353 meta = {
1211 1354 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -1225,13 +1368,13 b''
1225 1368 };
1226 1369 };
1227 1370 pyramid-debugtoolbar = super.buildPythonPackage {
1228 name = "pyramid-debugtoolbar-2.4.2";
1371 name = "pyramid-debugtoolbar-3.0.5";
1229 1372 buildInputs = with self; [];
1230 1373 doCheck = false;
1231 1374 propagatedBuildInputs = with self; [pyramid pyramid-mako repoze.lru Pygments];
1232 1375 src = fetchurl {
1233 url = "https://pypi.python.org/packages/89/00/ed5426ee41ed747ba3ffd30e8230841a6878286ea67d480b1444d24f06a2/pyramid_debugtoolbar-2.4.2.tar.gz";
1234 md5 = "073ea67086cc4bd5decc3a000853642d";
1376 url = "https://pypi.python.org/packages/64/0e/df00bfb55605900e7a2f7e4a18dd83575a6651688e297d5a0aa4c208fd7d/pyramid_debugtoolbar-3.0.5.tar.gz";
1377 md5 = "aebab8c3bfdc6f89e4d3adc1d126538e";
1235 1378 };
1236 1379 meta = {
1237 1380 license = [ { fullName = "Repoze Public License"; } pkgs.lib.licenses.bsdOriginal ];
@@ -1368,16 +1511,16 b''
1368 1511 };
1369 1512 };
1370 1513 python-dateutil = super.buildPythonPackage {
1371 name = "python-dateutil-1.5";
1514 name = "python-dateutil-2.1";
1372 1515 buildInputs = with self; [];
1373 1516 doCheck = false;
1374 propagatedBuildInputs = with self; [];
1517 propagatedBuildInputs = with self; [six];
1375 1518 src = fetchurl {
1376 url = "https://pypi.python.org/packages/b4/7c/df59c89a753eb33c7c44e1dd42de0e9bc2ccdd5a4d576e0bfad97cc280cb/python-dateutil-1.5.tar.gz";
1377 md5 = "0dcb1de5e5cad69490a3b6ab63f0cfa5";
1519 url = "https://pypi.python.org/packages/65/52/9c18dac21f174ad31b65e22d24297864a954e6fe65876eba3f5773d2da43/python-dateutil-2.1.tar.gz";
1520 md5 = "1534bb15cf311f07afaa3aacba1c028b";
1378 1521 };
1379 1522 meta = {
1380 license = [ pkgs.lib.licenses.psfl ];
1523 license = [ { fullName = "Simplified BSD"; } ];
1381 1524 };
1382 1525 };
1383 1526 python-editor = super.buildPythonPackage {
@@ -1498,10 +1641,10 b''
1498 1641 };
1499 1642 };
1500 1643 rhodecode-enterprise-ce = super.buildPythonPackage {
1501 name = "rhodecode-enterprise-ce-4.6.1";
1502 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage cssselect lxml configobj];
1644 name = "rhodecode-enterprise-ce-4.7.0";
1645 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
1503 1646 doCheck = true;
1504 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1647 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1505 1648 src = ./.;
1506 1649 meta = {
1507 1650 license = [ { fullName = "Affero GNU General Public License v3 or later (AGPLv3+)"; } { fullName = "AGPLv3, and Commercial License"; } ];
@@ -1520,19 +1663,6 b''
1520 1663 license = [ { fullName = "AGPLv3 and Proprietary"; } ];
1521 1664 };
1522 1665 };
1523 serpent = super.buildPythonPackage {
1524 name = "serpent-1.15";
1525 buildInputs = with self; [];
1526 doCheck = false;
1527 propagatedBuildInputs = with self; [];
1528 src = fetchurl {
1529 url = "https://pypi.python.org/packages/7b/38/b2b27673a882ff2ea5871bb3e3e6b496ebbaafd1612e51990ffb158b9254/serpent-1.15.tar.gz";
1530 md5 = "e27b1aad5c218e16442f52abb7c7053a";
1531 };
1532 meta = {
1533 license = [ pkgs.lib.licenses.mit ];
1534 };
1535 };
1536 1666 setproctitle = super.buildPythonPackage {
1537 1667 name = "setproctitle-1.1.8";
1538 1668 buildInputs = with self; [];
@@ -1650,14 +1780,27 b''
1650 1780 license = [ pkgs.lib.licenses.mit ];
1651 1781 };
1652 1782 };
1783 testpath = super.buildPythonPackage {
1784 name = "testpath-0.1";
1785 buildInputs = with self; [];
1786 doCheck = false;
1787 propagatedBuildInputs = with self; [];
1788 src = fetchurl {
1789 url = "https://pypi.python.org/packages/f9/c4/c0b22f35138bc26a6058c39cb61db1e8977e5e9550b12cd2cb02ef56fc51/testpath-0.1.tar.gz";
1790 md5 = "401918bcd0b0e5b71a9b909835117bc6";
1791 };
1792 meta = {
1793 license = [ pkgs.lib.licenses.mit ];
1794 };
1795 };
1653 1796 traitlets = super.buildPythonPackage {
1654 name = "traitlets-4.3.1";
1797 name = "traitlets-4.3.2";
1655 1798 buildInputs = with self; [];
1656 1799 doCheck = false;
1657 1800 propagatedBuildInputs = with self; [ipython-genutils six decorator enum34];
1658 1801 src = fetchurl {
1659 url = "https://pypi.python.org/packages/b1/d6/5b5aa6d5c474691909b91493da1e8972e309c9f01ecfe4aeafd272eb3234/traitlets-4.3.1.tar.gz";
1660 md5 = "dd0b1b6e5d31ce446d55a4b5e5083c98";
1802 url = "https://pypi.python.org/packages/a5/98/7f5ef2fe9e9e071813aaf9cb91d1a732e0a68b6c44a32b38cb8e14c3f069/traitlets-4.3.2.tar.gz";
1803 md5 = "3068663f2f38fd939a9eb3a500ccc154";
1661 1804 };
1662 1805 meta = {
1663 1806 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -2,7 +2,6 b''
2 2 testpaths = ./rhodecode
3 3 pylons_config = rhodecode/tests/rhodecode.ini
4 4 vcsserver_protocol = http
5 vcsserver_config_pyro4 = rhodecode/tests/vcsserver_pyro4.ini
6 5 vcsserver_config_http = rhodecode/tests/vcsserver_http.ini
7 6 norecursedirs = tests/scripts
8 7 addopts = -k "not _BaseTest"
@@ -14,7 +14,8 b' channelstream==0.5.2'
14 14 click==5.1
15 15 colander==1.2
16 16 configobj==5.0.6
17 decorator==3.4.2
17 cssselect==1.0.1
18 decorator==4.0.11
18 19 deform==2.0a2
19 20 docutils==0.12
20 21 dogpile.cache==0.6.1
@@ -29,6 +30,7 b' iso8601==0.1.11'
29 30 itsdangerous==0.24
30 31 Jinja2==2.7.3
31 32 kombu==1.5.1
33 lxml==3.7.3
32 34 Mako==1.0.6
33 35 Markdown==2.6.7
34 36 MarkupSafe==0.23
@@ -42,6 +44,7 b' paramiko==1.15.1'
42 44 Paste==2.0.3
43 45 PasteDeploy==1.5.2
44 46 PasteScript==1.7.5
47 pathlib2==2.1.0
45 48 psutil==4.3.1
46 49 psycopg2==2.6.1
47 50 py-bcrypt==0.4
@@ -52,12 +55,12 b' pygments-markdown-lexer==0.1.0.dev39'
52 55 Pygments==2.2.0
53 56 pyparsing==1.5.7
54 57 pyramid-beaker==0.8
55 pyramid-debugtoolbar==2.4.2
58 pyramid-debugtoolbar==3.0.5
56 59 pyramid-jinja2==2.5
57 60 pyramid-mako==1.0.2
58 pyramid==1.6.1
61 pyramid==1.7.4
59 62 pysqlite==2.6.3
60 python-dateutil==1.5
63 python-dateutil==2.1
61 64 python-ldap==2.4.19
62 65 python-memcached==1.57
63 66 python-pam==1.8.2
@@ -97,6 +100,12 b' https://code.rhodecode.com/upstream/pylo'
97 100 # not released py-gfm==0.1.3
98 101 https://code.rhodecode.com/upstream/py-gfm/archive/0d66a19bc16e3d49de273c0f797d4e4781e8c0f2.tar.gz?md5=0d0d5385bfb629eea636a80b9c2bfd16#egg=py-gfm==0.1.3.rhodecode-upstream1
99 102
103 # IPYTHON RENDERING
104 # entrypoints backport, pypi version doesn't support egg installs
105 https://code.rhodecode.com/upstream/entrypoints/archive/96e6d645684e1af3d7df5b5272f3fe85a546b233.tar.gz?md5=7db37771aea9ac9fefe093e5d6987313#egg=entrypoints==0.2.2.rhodecode-upstream1
106 nbconvert==5.1.1
107 nbformat==4.3.0
108 jupyter_client==5.0.0
100 109
101 110 ## cli tools
102 111 alembic==0.8.4
@@ -123,9 +132,5 b' https://code.rhodecode.com/rhodecode-too'
123 132 ## appenlight
124 133 appenlight-client==0.6.14
125 134
126 # Pyro/Deprecated TODO(Marcink): remove in 4.7 release.
127 Pyro4==4.41
128 serpent==1.15
129
130 135 ## test related requirements
131 136 -r requirements_test.txt
@@ -13,5 +13,3 b' mock==1.0.1'
13 13 WebTest==1.4.3
14 14 cov-core==1.15.0
15 15 coverage==3.7.1
16 cssselect==0.9.1
17 lxml==3.4.4
@@ -1,1 +1,1 b''
1 4.6.1 No newline at end of file
1 4.7.0 No newline at end of file
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 64 # defines current db version for migrations
54 __dbversion__ = 71 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
@@ -22,6 +22,7 b' import inspect'
22 22 import itertools
23 23 import logging
24 24 import types
25 import fnmatch
25 26
26 27 import decorator
27 28 import venusian
@@ -47,6 +48,18 b" DEFAULT_RENDERER = 'jsonrpc_renderer'"
47 48 DEFAULT_URL = '/_admin/apiv2'
48 49
49 50
51 def find_methods(jsonrpc_methods, pattern):
52 matches = OrderedDict()
53 if not isinstance(pattern, (list, tuple)):
54 pattern = [pattern]
55
56 for single_pattern in pattern:
57 for method_name, method in jsonrpc_methods.items():
58 if fnmatch.fnmatch(method_name, single_pattern):
59 matches[method_name] = method
60 return matches
61
62
50 63 class ExtJsonRenderer(object):
51 64 """
52 65 Custom renderer that mkaes use of our ext_json lib
@@ -143,7 +156,19 b' def exception_view(exc, request):'
143 156 log.debug('json-rpc method `%s` not found in list of '
144 157 'api calls: %s, rpc_id:%s',
145 158 method, request.registry.jsonrpc_methods.keys(), rpc_id)
146 fault_message = "No such method: {}".format(method)
159
160 similar = 'none'
161 try:
162 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 similar_found = find_methods(
164 request.registry.jsonrpc_methods, similar_paterns)
165 similar = ', '.join(similar_found.keys()) or similar
166 except Exception:
167 # make the whole above block safe
168 pass
169
170 fault_message = "No such method: {}. Similar methods: {}".format(
171 method, similar)
147 172
148 173 return jsonrpc_error(request, fault_message, rpc_id)
149 174
@@ -184,18 +209,18 b' def request_view(request):'
184 209 request.rpc_user = auth_u
185 210
186 211 # now check if token is valid for API
187 role = UserApiKeys.ROLE_API
188 extra_auth_tokens = [
189 x.api_key for x in User.extra_valid_auth_tokens(api_user, role=role)]
190 active_tokens = [api_user.api_key] + extra_auth_tokens
212 auth_token = request.rpc_api_key
213 token_match = api_user.authenticate_by_token(
214 auth_token, roles=[UserApiKeys.ROLE_API])
215 invalid_token = not token_match
191 216
192 log.debug('Checking if API key has proper role')
193 if request.rpc_api_key not in active_tokens:
217 log.debug('Checking if API KEY is valid with proper role')
218 if invalid_token:
194 219 return jsonrpc_error(
195 220 request, retid=request.rpc_id,
196 message='API KEY has bad role for an API call')
221 message='API KEY invalid or, has bad role for an API call')
197 222
198 except Exception as e:
223 except Exception:
199 224 log.exception('Error on API AUTH')
200 225 return jsonrpc_error(
201 226 request, retid=request.rpc_id, message='Invalid API KEY')
@@ -351,9 +376,10 b' class RoutePredicate(object):'
351 376 class NotFoundPredicate(object):
352 377 def __init__(self, val, config):
353 378 self.val = val
379 self.methods = config.registry.jsonrpc_methods
354 380
355 381 def text(self):
356 return 'jsonrpc method not found = %s' % self.val
382 return 'jsonrpc method not found = {}.'.format(self.val)
357 383
358 384 phash = text
359 385
@@ -22,14 +22,19 b' import pytest'
22 22
23 23 from rhodecode.model.meta import Session
24 24 from rhodecode.model.user import UserModel
25 from rhodecode.model.auth_token import AuthTokenModel
25 26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 27
27 28
28 29 @pytest.fixture(scope="class")
29 30 def testuser_api(request, pylonsapp):
30 31 cls = request.cls
32
33 # ADMIN USER
31 34 cls.usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
32 35 cls.apikey = cls.usr.api_key
36
37 # REGULAR USER
33 38 cls.test_user = UserModel().create_or_update(
34 39 username='test-api',
35 40 password='test',
@@ -37,6 +42,11 b' def testuser_api(request, pylonsapp):'
37 42 firstname='first',
38 43 lastname='last'
39 44 )
45 # create TOKEN for user, if he doesn't have one
46 if not cls.test_user.api_key:
47 AuthTokenModel().create(
48 user=cls.test_user, description='TEST_USER_TOKEN')
49
40 50 Session().commit()
41 51 cls.TEST_USER_LOGIN = cls.test_user.username
42 52 cls.apikey_regular = cls.test_user.api_key
@@ -80,7 +80,13 b' class TestApi(object):'
80 80 def test_api_non_existing_method(self, request):
81 81 id_, params = build_data(self.apikey, 'not_existing', args='xx')
82 82 response = api_call(self.app, params)
83 expected = 'No such method: not_existing'
83 expected = 'No such method: not_existing. Similar methods: none'
84 assert_error(id_, expected, given=response.body)
85
86 def test_api_non_existing_method_have_similar(self, request):
87 id_, params = build_data(self.apikey, 'comment', args='xx')
88 response = api_call(self.app, params)
89 expected = 'No such method: comment. Similar methods: changeset_comment, comment_pull_request, comment_commit'
84 90 assert_error(id_, expected, given=response.body)
85 91
86 92 def test_api_disabled_user(self, request):
@@ -29,6 +29,7 b' from rhodecode.api.tests.utils import ('
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestClosePullRequest(object):
32
32 33 @pytest.mark.backends("git", "hg")
33 34 def test_api_close_pull_request(self, pr_util):
34 35 pull_request = pr_util.create_pull_request()
@@ -53,11 +53,10 b' class TestApiDeleteRepo(object):'
53 53 }
54 54 assert_ok(id_, expected, given=response.body)
55 55
56 def test_api_delete_repo_by_non_admin_no_permission(
57 self, backend, user_regular):
56 def test_api_delete_repo_by_non_admin_no_permission(self, backend):
58 57 repo = backend.create_repo()
59 58 id_, params = build_data(
60 user_regular.api_key, 'delete_repo', repoid=repo.repo_name, )
59 self.apikey_regular, 'delete_repo', repoid=repo.repo_name, )
61 60 response = api_call(self.app, params)
62 61 expected = 'repository `%s` does not exist' % (repo.repo_name)
63 62 assert_error(id_, expected, given=response.body)
@@ -23,7 +23,7 b' import pytest'
23 23
24 24 from rhodecode.model.user import UserModel
25 25 from rhodecode.model.user_group import UserGroupModel
26 from rhodecode.tests import TEST_USER_REGULAR_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_EMAIL
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_error, assert_ok, crash, jsonify)
29 29
@@ -33,7 +33,8 b' class TestUpdateUserGroup(object):'
33 33 @pytest.mark.parametrize("changing_attr, updates", [
34 34 ('group_name', {'group_name': 'new_group_name'}),
35 35 ('group_name', {'group_name': 'test_group_for_update'}),
36 ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
36 # ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
37 ('owner_email', {'owner_email': TEST_USER_ADMIN_EMAIL}),
37 38 ('active', {'active': False}),
38 39 ('active', {'active': True})
39 40 ])
@@ -59,7 +60,8 b' class TestUpdateUserGroup(object):'
59 60 # TODO: mikhail: decide if we need to test against the commented params
60 61 # ('group_name', {'group_name': 'new_group_name'}),
61 62 # ('group_name', {'group_name': 'test_group_for_update'}),
62 ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
63 # ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
64 ('owner_email', {'owner_email': TEST_USER_ADMIN_EMAIL}),
63 65 ('active', {'active': False}),
64 66 ('active', {'active': True})
65 67 ])
@@ -181,29 +181,56 b' class TestGetRefHash(object):'
181 181 class TestUserByNameOrError(object):
182 182 def test_user_found_by_id(self):
183 183 fake_user = Mock(id=123)
184
185 patcher = patch('rhodecode.model.user.UserModel.get_user')
186 with patcher as get_user:
187 get_user.return_value = fake_user
188
189 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
190 with patcher as get_by_username:
191 result = utils.get_user_or_error(123)
192 assert result == fake_user
193
194 def test_user_not_found_by_id_as_str(self):
195 fake_user = Mock(id=123)
196
184 197 patcher = patch('rhodecode.model.user.UserModel.get_user')
185 198 with patcher as get_user:
186 199 get_user.return_value = fake_user
187 result = utils.get_user_or_error('123')
188 assert result == fake_user
200 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
201 with patcher as get_by_username:
202 get_by_username.return_value = None
203
204 with pytest.raises(JSONRPCError):
205 utils.get_user_or_error('123')
189 206
190 207 def test_user_found_by_name(self):
191 208 fake_user = Mock(id=123)
192 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
193 with patcher as get_by_username:
194 get_by_username.return_value = fake_user
195 result = utils.get_user_or_error('test')
196 assert result == fake_user
209
210 patcher = patch('rhodecode.model.user.UserModel.get_user')
211 with patcher as get_user:
212 get_user.return_value = None
213
214 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
215 with patcher as get_by_username:
216 get_by_username.return_value = fake_user
217
218 result = utils.get_user_or_error('test')
219 assert result == fake_user
197 220
198 221 def test_user_not_found_by_id(self):
199 222 patcher = patch('rhodecode.model.user.UserModel.get_user')
200 223 with patcher as get_user:
201 224 get_user.return_value = None
202 with pytest.raises(JSONRPCError) as excinfo:
203 utils.get_user_or_error('123')
225 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
226 with patcher as get_by_username:
227 get_by_username.return_value = None
204 228
205 expected_message = 'user `123` does not exist'
206 assert excinfo.value.message == expected_message
229 with pytest.raises(JSONRPCError) as excinfo:
230 utils.get_user_or_error(123)
231
232 expected_message = 'user `123` does not exist'
233 assert excinfo.value.message == expected_message
207 234
208 235 def test_user_not_found_by_name(self):
209 236 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
@@ -216,8 +243,7 b' class TestUserByNameOrError(object):'
216 243 assert excinfo.value.message == expected_message
217 244
218 245
219 class TestGetCommitDict:
220
246 class TestGetCommitDict(object):
221 247 @pytest.mark.parametrize('filename, expected', [
222 248 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
223 249 (b'sp\xa4cial', u'sp\ufffdcial'),
@@ -213,15 +213,19 b' def get_user_or_error(userid):'
213 213 :param userid:
214 214 """
215 215 from rhodecode.model.user import UserModel
216 user_model = UserModel()
216 217
217 user_model = UserModel()
218 try:
219 user = user_model.get_user(int(userid))
220 except ValueError:
218 if isinstance(userid, (int, long)):
219 try:
220 user = user_model.get_user(userid)
221 except ValueError:
222 user = None
223 else:
221 224 user = user_model.get_by_username(userid)
222 225
223 226 if user is None:
224 raise JSONRPCError("user `%s` does not exist" % (userid,))
227 raise JSONRPCError(
228 'user `%s` does not exist' % (userid,))
225 229 return user
226 230
227 231
@@ -232,10 +236,19 b' def get_repo_or_error(repoid):'
232 236 :param repoid:
233 237 """
234 238 from rhodecode.model.repo import RepoModel
239 repo_model = RepoModel()
235 240
236 repo = RepoModel().get_repo(repoid)
241 if isinstance(repoid, (int, long)):
242 try:
243 repo = repo_model.get_repo(repoid)
244 except ValueError:
245 repo = None
246 else:
247 repo = repo_model.get_by_repo_name(repoid)
248
237 249 if repo is None:
238 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
250 raise JSONRPCError(
251 'repository `%s` does not exist' % (repoid,))
239 252 return repo
240 253
241 254
@@ -246,8 +259,16 b' def get_repo_group_or_error(repogroupid)'
246 259 :param repogroupid:
247 260 """
248 261 from rhodecode.model.repo_group import RepoGroupModel
262 repo_group_model = RepoGroupModel()
249 263
250 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
264 if isinstance(repogroupid, (int, long)):
265 try:
266 repo_group = repo_group_model._get_repo_group(repogroupid)
267 except ValueError:
268 repo_group = None
269 else:
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
271
251 272 if repo_group is None:
252 273 raise JSONRPCError(
253 274 'repository group `%s` does not exist' % (repogroupid,))
@@ -261,10 +282,19 b' def get_user_group_or_error(usergroupid)'
261 282 :param usergroupid:
262 283 """
263 284 from rhodecode.model.user_group import UserGroupModel
285 user_group_model = UserGroupModel()
264 286
265 user_group = UserGroupModel().get_group(usergroupid)
287 if isinstance(usergroupid, (int, long)):
288 try:
289 user_group = user_group_model.get_group(usergroupid)
290 except ValueError:
291 user_group = None
292 else:
293 user_group = user_group_model.get_by_name(usergroupid)
294
266 295 if user_group is None:
267 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
296 raise JSONRPCError(
297 'user group `%s` does not exist' % (usergroupid,))
268 298 return user_group
269 299
270 300
@@ -244,9 +244,8 b' def merge_pull_request(request, apiuser,'
244 244
245 245 .. code-block:: bash
246 246
247 "id": <id_given_in_input>,
248 "result":
249 {
247 "id": <id_given_in_input>,
248 "result": {
250 249 "executed": "<bool>",
251 250 "failure_reason": "<int>",
252 251 "merge_commit_id": "<merge_commit_id>",
@@ -257,8 +256,7 b' def merge_pull_request(request, apiuser,'
257 256 "name": "<name>"
258 257 }
259 258 },
260 "error": null
261
259 "error": null
262 260 """
263 261 repo = get_repo_or_error(repoid)
264 262 if not isinstance(userid, Optional):
@@ -321,13 +319,12 b' def close_pull_request(request, apiuser,'
321 319
322 320 .. code-block:: bash
323 321
324 "id": <id_given_in_input>,
325 "result":
326 {
322 "id": <id_given_in_input>,
323 "result": {
327 324 "pull_request_id": "<int>",
328 325 "closed": "<bool>"
329 326 },
330 "error": null
327 "error": null
331 328
332 329 """
333 330 repo = get_repo_or_error(repoid)
@@ -396,15 +393,14 b' def comment_pull_request('
396 393
397 394 .. code-block:: bash
398 395
399 id : <id_given_in_input>
400 result :
401 {
396 id : <id_given_in_input>
397 result : {
402 398 "pull_request_id": "<Integer>",
403 399 "comment_id": "<Integer>",
404 400 "status": {"given": <given_status>,
405 401 "was_changed": <bool status_was_actually_changed> },
406 }
407 error : null
402 },
403 error : null
408 404 """
409 405 repo = get_repo_or_error(repoid)
410 406 if not isinstance(userid, Optional):
@@ -535,9 +531,8 b' def create_pull_request('
535 531 :param reviewers: Set the new pull request reviewers list.
536 532 :type reviewers: Optional(list)
537 533 Accepts username strings or objects of the format:
538 {
539 'username': 'nick', 'reasons': ['original author']
540 }
534
535 {'username': 'nick', 'reasons': ['original author']}
541 536 """
542 537
543 538 source = get_repo_or_error(source_repo)
@@ -633,9 +628,8 b' def update_pull_request('
633 628
634 629 .. code-block:: bash
635 630
636 id : <id_given_in_input>
637 result :
638 {
631 id : <id_given_in_input>
632 result : {
639 633 "msg": "Updated pull request `63`",
640 634 "pull_request": <pull_request_object>,
641 635 "updated_reviewers": {
@@ -655,7 +649,7 b' def update_pull_request('
655 649 "removed": []
656 650 }
657 651 }
658 error : null
652 error : null
659 653 """
660 654
661 655 repo = get_repo_or_error(repoid)
@@ -18,10 +18,12 b''
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 inspect
22 22 import logging
23 import itertools
23 24
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
25 27
26 28 from rhodecode.api.utils import (
27 29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
@@ -243,3 +245,77 b' def cleanup_sessions(request, apiuser, o'
243 245 raise JSONRPCError(
244 246 'Error occurred during session cleanup'
245 247 )
248
249
250 @jsonrpc_method()
251 def get_method(request, apiuser, pattern=Optional('*')):
252 """
253 Returns list of all available API methods. By default match pattern
254 os "*" but any other pattern can be specified. eg *comment* will return
255 all methods with comment inside them. If just single method is matched
256 returned data will also include method specification
257
258 This command can only be run using an |authtoken| with admin rights to
259 the specified repository.
260
261 This command takes the following options:
262
263 :param apiuser: This is filled automatically from the |authtoken|.
264 :type apiuser: AuthUser
265 :param pattern: pattern to match method names against
266 :type older_then: Optional("*")
267
268 Example output:
269
270 .. code-block:: bash
271
272 id : <id_given_in_input>
273 "result": [
274 "changeset_comment",
275 "comment_pull_request",
276 "comment_commit"
277 ]
278 error : null
279
280 .. code-block:: bash
281
282 id : <id_given_in_input>
283 "result": [
284 "comment_commit",
285 {
286 "apiuser": "<RequiredType>",
287 "comment_type": "<Optional:u'note'>",
288 "commit_id": "<RequiredType>",
289 "message": "<RequiredType>",
290 "repoid": "<RequiredType>",
291 "request": "<RequiredType>",
292 "resolves_comment_id": "<Optional:None>",
293 "status": "<Optional:None>",
294 "userid": "<Optional:<OptionalAttr:apiuser>>"
295 }
296 ]
297 error : null
298 """
299 if not has_superadmin_permission(apiuser):
300 raise JSONRPCForbidden()
301
302 pattern = Optional.extract(pattern)
303
304 matches = find_methods(request.registry.jsonrpc_methods, pattern)
305
306 args_desc = []
307 if len(matches) == 1:
308 func = matches[matches.keys()[0]]
309
310 argspec = inspect.getargspec(func)
311 arglist = argspec[0]
312 defaults = map(repr, argspec[3] or [])
313
314 default_empty = '<RequiredType>'
315
316 # kw arguments required by this method
317 func_kwargs = dict(itertools.izip_longest(
318 reversed(arglist), reversed(defaults), fillvalue=default_empty))
319 args_desc.append(func_kwargs)
320
321 return matches.keys() + args_desc
@@ -29,7 +29,6 b' from rhodecode.lib.utils2 import safe_in'
29 29 from rhodecode.model.db import Session, User, Repository
30 30 from rhodecode.model.user import UserModel
31 31
32
33 32 log = logging.getLogger(__name__)
34 33
35 34
@@ -62,8 +61,8 b' def get_user(request, apiuser, userid=Op'
62 61 "result": {
63 62 "active": true,
64 63 "admin": false,
65 "api_key": "api-key",
66 64 "api_keys": [ list of keys ],
65 "auth_tokens": [ list of tokens with details ],
67 66 "email": "user@example.com",
68 67 "emails": [
69 68 "user@example.com"
@@ -74,6 +73,7 b' def get_user(request, apiuser, userid=Op'
74 73 "ip_addresses": [],
75 74 "language": null,
76 75 "last_login": "Timestamp",
76 "last_activity": "Timestamp",
77 77 "lastname": "surnae",
78 78 "permissions": {
79 79 "global": [
@@ -133,7 +133,7 b' def get_users(request, apiuser):'
133 133 .. code-block:: bash
134 134
135 135 id : <id_given_in_input>
136 result: [<user_object>, ...]
136 result: [<user_object>, ...]
137 137 error: null
138 138 """
139 139
@@ -191,15 +191,16 b' def create_user(request, apiuser, userna'
191 191 :type force_password_change: Optional(``True`` | ``False``)
192 192 :param create_personal_repo_group: Create personal repo group for this user
193 193 :type create_personal_repo_group: Optional(``True`` | ``False``)
194
194 195 Example output:
195 196
196 197 .. code-block:: bash
197 198
198 199 id : <id_given_in_input>
199 200 result: {
200 "msg" : "created new user `<username>`",
201 "user": <user_obj>
202 }
201 "msg" : "created new user `<username>`",
202 "user": <user_obj>
203 }
203 204 error: null
204 205
205 206 Example error output:
@@ -305,9 +306,9 b' def update_user(request, apiuser, userid'
305 306
306 307 id : <id_given_in_input>
307 308 result: {
308 "msg" : "updated user ID:<userid> <username>",
309 "user": <user_object>,
310 }
309 "msg" : "updated user ID:<userid> <username>",
310 "user": <user_object>,
311 }
311 312 error: null
312 313
313 314 Example error output:
@@ -384,9 +385,9 b' def delete_user(request, apiuser, userid'
384 385
385 386 id : <id_given_in_input>
386 387 result: {
387 "msg" : "deleted user ID:<userid> <username>",
388 "user": null
389 }
388 "msg" : "deleted user ID:<userid> <username>",
389 "user": null
390 }
390 391 error: null
391 392
392 393 Example error output:
@@ -470,3 +471,45 b' def get_user_locks(request, apiuser, use'
470 471 ret.append(_api_data)
471 472
472 473 return ret
474
475
476 @jsonrpc_method()
477 def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))):
478 """
479 Fetches all action logs made by the specified user.
480
481 This command takes the following options:
482
483 :param apiuser: This is filled automatically from the |authtoken|.
484 :type apiuser: AuthUser
485 :param userid: Sets the userid whose list of locked |repos| will be
486 displayed.
487 :type userid: Optional(str or int)
488
489 Example output:
490
491 .. code-block:: bash
492
493 id : <id_given_in_input>
494 result : {
495 [action, action,...]
496 }
497 error : null
498 """
499
500 if not has_superadmin_permission(apiuser):
501 # make sure normal user does not pass someone else userid,
502 # he is not allowed to do that
503 if not isinstance(userid, Optional) and userid != apiuser.user_id:
504 raise JSONRPCError('userid is not the same as your user')
505
506 userid = Optional.extract(userid, evaluate_locals=locals())
507 userid = getattr(userid, 'user_id', userid)
508 user = get_user_or_error(userid)
509
510 ret = []
511
512 # show all user actions
513 for entry in UserModel().get_user_log(user, filter_term=None):
514 ret.append(entry)
515 return ret
@@ -19,11 +19,72 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 from rhodecode.admin.navigation import NavigationRegistry
22 from rhodecode.apps.admin.navigation import NavigationRegistry
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 def admin_routes(config):
28 """
29 Admin prefixed routes
30 """
31
32 config.add_route(
33 name='admin_settings_open_source',
34 pattern='/settings/open_source')
35 config.add_route(
36 name='admin_settings_vcs_svn_generate_cfg',
37 pattern='/settings/vcs/svn_generate_cfg')
38
39 config.add_route(
40 name='admin_settings_system',
41 pattern='/settings/system')
42 config.add_route(
43 name='admin_settings_system_update',
44 pattern='/settings/system/updates')
45
46 config.add_route(
47 name='admin_settings_sessions',
48 pattern='/settings/sessions')
49 config.add_route(
50 name='admin_settings_sessions_cleanup',
51 pattern='/settings/sessions/cleanup')
52
53 # users admin
54 config.add_route(
55 name='users',
56 pattern='/users')
57
58 config.add_route(
59 name='users_data',
60 pattern='/users_data')
61
62 # user auth tokens
63 config.add_route(
64 name='edit_user_auth_tokens',
65 pattern='/users/{user_id:\d+}/edit/auth_tokens')
66 config.add_route(
67 name='edit_user_auth_tokens_add',
68 pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
69 config.add_route(
70 name='edit_user_auth_tokens_delete',
71 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
72
73 # user groups management
74 config.add_route(
75 name='edit_user_groups_management',
76 pattern='/users/{user_id:\d+}/edit/groups_management')
77
78 config.add_route(
79 name='edit_user_groups_management_updates',
80 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
81
82 # user audit logs
83 config.add_route(
84 name='edit_user_audit_logs',
85 pattern='/users/{user_id:\d+}/edit/audit')
86
87
27 88 def includeme(config):
28 89 settings = config.get_settings()
29 90
@@ -32,26 +93,7 b' def includeme(config):'
32 93 navigation_registry = NavigationRegistry(labs_active=labs_active)
33 94 config.registry.registerUtility(navigation_registry)
34 95
35 config.add_route(
36 name='admin_settings_open_source',
37 pattern=ADMIN_PREFIX + '/settings/open_source')
38 config.add_route(
39 name='admin_settings_vcs_svn_generate_cfg',
40 pattern=ADMIN_PREFIX + '/settings/vcs/svn_generate_cfg')
41
42 config.add_route(
43 name='admin_settings_system',
44 pattern=ADMIN_PREFIX + '/settings/system')
45 config.add_route(
46 name='admin_settings_system_update',
47 pattern=ADMIN_PREFIX + '/settings/system/updates')
48
49 config.add_route(
50 name='admin_settings_sessions',
51 pattern=ADMIN_PREFIX + '/settings/sessions')
52 config.add_route(
53 name='admin_settings_sessions_cleanup',
54 pattern=ADMIN_PREFIX + '/settings/sessions/cleanup')
96 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
55 97
56 98 # Scan module for configuration decorators.
57 99 config.scan()
1 NO CONTENT: file renamed from rhodecode/admin/interfaces.py to rhodecode/apps/admin/interfaces.py
@@ -25,7 +25,7 b' import collections'
25 25 from pylons import url
26 26 from zope.interface import implementer
27 27
28 from rhodecode.admin.interfaces import IAdminNavigationRegistry
28 from rhodecode.apps.admin.interfaces import IAdminNavigationRegistry
29 29 from rhodecode.lib.utils import get_registry
30 30 from rhodecode.translation import _
31 31
1 NO CONTENT: file renamed from rhodecode/admin/views/__init__.py to rhodecode/apps/admin/views/__init__.py
@@ -24,15 +24,15 b' import logging'
24 24 from pylons import tmpl_context as c
25 25 from pyramid.view import view_config
26 26
27 from rhodecode.admin.views.base import AdminSettingsView
28 from rhodecode.admin.navigation import navigation_list
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps.admin.navigation import navigation_list
29 29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 30 from rhodecode.lib.utils import read_opensource_licenses
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 class OpenSourceLicensesAdminSettingsView(AdminSettingsView):
35 class OpenSourceLicensesAdminSettingsView(BaseAppView):
36 36
37 37 @LoginRequired()
38 38 @HasPermissionAllDecorator('hg.admin')
@@ -24,9 +24,8 b' from pylons import tmpl_context as c'
24 24 from pyramid.view import view_config
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 from rhodecode.translation import _
28
29 from rhodecode.admin.views.base import AdminSettingsView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps.admin.navigation import navigation_list
30 29 from rhodecode.lib.auth import (
31 30 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 31 from rhodecode.lib.utils2 import safe_int
@@ -34,13 +33,10 b' from rhodecode.lib import system_info'
34 33 from rhodecode.lib import user_sessions
35 34
36 35
37 from rhodecode.admin.navigation import navigation_list
38
39
40 36 log = logging.getLogger(__name__)
41 37
42 38
43 class AdminSessionSettingsView(AdminSettingsView):
39 class AdminSessionSettingsView(BaseAppView):
44 40
45 41 @LoginRequired()
46 42 @HasPermissionAllDecorator('hg.admin')
@@ -22,16 +22,15 b' import logging'
22 22
23 23 from pyramid.view import view_config
24 24
25 from rhodecode.svn_support.utils import generate_mod_dav_svn_config
26
27 from rhodecode.admin.views.base import AdminSettingsView
25 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
28 27 from rhodecode.lib.auth import (
29 28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 29
31 30 log = logging.getLogger(__name__)
32 31
33 32
34 class SvnConfigAdminSettingsView(AdminSettingsView):
33 class SvnConfigAdminSettingsView(BaseAppView):
35 34
36 35 @LoginRequired()
37 36 @CSRFRequired()
@@ -26,20 +26,19 b' from pylons import tmpl_context as c'
26 26 from pyramid.view import view_config
27 27
28 28 import rhodecode
29 from rhodecode.apps._base import BaseAppView
30 from rhodecode.apps.admin.navigation import navigation_list
29 31 from rhodecode.lib import helpers as h
30 32 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
31 33 from rhodecode.lib.utils2 import str2bool
32 34 from rhodecode.lib import system_info
33 35 from rhodecode.lib.ext_json import json
34
35 from rhodecode.admin.views.base import AdminSettingsView
36 from rhodecode.admin.navigation import navigation_list
37 36 from rhodecode.model.settings import SettingsModel
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class AdminSystemInfoSettingsView(AdminSettingsView):
41 class AdminSystemInfoSettingsView(BaseAppView):
43 42
44 43 @staticmethod
45 44 def get_update_data(update_url):
@@ -107,6 +106,8 b' class AdminSystemInfoSettingsView(AdminS'
107 106 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
108 107 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
109 108 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
109 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
110 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
110 111 ('', '', ''), # spacer
111 112
112 113 # Database
@@ -122,7 +123,7 b' class AdminSystemInfoSettingsView(AdminS'
122 123 ('', '', ''), # spacer
123 124
124 125 # Systems stats
125 (_('CPU'), val('cpu'), state('cpu')),
126 (_('CPU'), val('cpu')['text'], state('cpu')),
126 127 (_('Load'), val('load')['text'], state('load')),
127 128 (_('Memory'), val('memory')['text'], state('memory')),
128 129 (_('Uptime'), val('uptime')['text'], state('uptime')),
@@ -85,4 +85,6 b' def includeme(config):'
85 85 config.add_route(
86 86 name='channelstream_proxy',
87 87 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
88 config.scan('rhodecode.channelstream')
88
89 # Scan module for configuration decorators.
90 config.scan()
1 NO CONTENT: file renamed from rhodecode/channelstream/views.py to rhodecode/apps/channelstream/views.py
1 NO CONTENT: file renamed from rhodecode/login/__init__.py to rhodecode/apps/login/__init__.py
1 NO CONTENT: file renamed from rhodecode/login/tests/__init__.py to rhodecode/apps/login/tests/__init__.py
@@ -22,8 +22,9 b''
22 22 import mock
23 23 import pytest
24 24
25 from rhodecode.apps.login.views import LoginView, CaptchaData
25 26 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.login.views import LoginView, CaptchaData
27 from rhodecode.lib.utils2 import AttributeDict
27 28 from rhodecode.model.settings import SettingsModel
28 29 from rhodecode.tests.utils import AssertResponse
29 30
@@ -40,7 +41,7 b' class RhodeCodeSetting(object):'
40 41 model.create_or_update_setting(name=self.name, val=self.value)
41 42 return self
42 43
43 def __exit__(self, type, value, traceback):
44 def __exit__(self, exc_type, exc_val, exc_tb):
44 45 model = SettingsModel()
45 46 if self.old_setting:
46 47 model.create_or_update_setting(
@@ -57,8 +58,12 b' class TestRegisterCaptcha(object):'
57 58 ('privkey', '', CaptchaData(True, 'privkey', '')),
58 59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
59 60 ])
60 def test_get_captcha_data(self, private_key, public_key, expected, db):
61 login_view = LoginView(mock.Mock(), mock.Mock())
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
62 request_stub, user_util):
63 request_stub.user = user_util.create_user().AuthUser
64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 login_view = LoginView(mock.Mock(), request_stub)
66
62 67 with RhodeCodeSetting('captcha_private_key', private_key):
63 68 with RhodeCodeSetting('captcha_public_key', public_key):
64 69 captcha = login_view._get_captcha_data()
@@ -92,7 +97,7 b' class TestRegisterCaptcha(object):'
92 97 assertr.no_element_exists('#recaptcha_field')
93 98
94 99 @pytest.mark.parametrize('valid', [False, True])
95 @mock.patch('rhodecode.login.views.submit')
100 @mock.patch('rhodecode.apps.login.views.submit')
96 101 @mock.patch.object(LoginView, '_get_captcha_data')
97 102 def test_register_with_active_captcha(
98 103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
@@ -18,6 +18,7 b''
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 import time
21 22 import collections
22 23 import datetime
23 24 import formencode
@@ -29,6 +30,7 b' from pyramid.httpexceptions import HTTPF'
29 30 from pyramid.view import view_config
30 31 from recaptcha.client.captcha import submit
31 32
33 from rhodecode.apps._base import BaseAppView
32 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 35 from rhodecode.events import UserRegistered
34 36 from rhodecode.lib import helpers as h
@@ -37,9 +39,10 b' from rhodecode.lib.auth import ('
37 39 from rhodecode.lib.base import get_ip_addr
38 40 from rhodecode.lib.exceptions import UserCreationError
39 41 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
42 from rhodecode.model.db import User, UserApiKeys
41 43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
42 44 from rhodecode.model.meta import Session
45 from rhodecode.model.auth_token import AuthTokenModel
43 46 from rhodecode.model.settings import SettingsModel
44 47 from rhodecode.model.user import UserModel
45 48 from rhodecode.translation import _
@@ -103,20 +106,13 b' def get_came_from(request):'
103 106 return came_from or url('home')
104 107
105 108
106 class LoginView(object):
109 class LoginView(BaseAppView):
107 110
108 def __init__(self, context, request):
109 self.request = request
110 self.context = context
111 self.session = request.session
112 self._rhodecode_user = request.user
113
114 def _get_template_context(self):
115 return {
116 'came_from': get_came_from(self.request),
117 'defaults': {},
118 'errors': {},
119 }
111 def load_default_context(self):
112 c = self._get_local_tmpl_context()
113 c.came_from = get_came_from(self.request)
114 self._register_global_c(c)
115 return c
120 116
121 117 def _get_captcha_data(self):
122 118 settings = SettingsModel().get_all_settings()
@@ -130,12 +126,13 b' class LoginView(object):'
130 126 route_name='login', request_method='GET',
131 127 renderer='rhodecode:templates/login.mako')
132 128 def login(self):
133 came_from = get_came_from(self.request)
134 user = self.request.user
129 c = self.load_default_context()
130 auth_user = self._rhodecode_user
135 131
136 132 # redirect if already logged in
137 if user.is_authenticated and not user.is_default and user.ip_allowed:
138 raise HTTPFound(came_from)
133 if (auth_user.is_authenticated and
134 not auth_user.is_default and auth_user.ip_allowed):
135 raise HTTPFound(c.came_from)
139 136
140 137 # check if we use headers plugin, and try to login using it.
141 138 try:
@@ -145,18 +142,18 b' class LoginView(object):'
145 142 if auth_info:
146 143 headers = _store_user_in_session(
147 144 self.session, auth_info.get('username'))
148 raise HTTPFound(came_from, headers=headers)
145 raise HTTPFound(c.came_from, headers=headers)
149 146 except UserCreationError as e:
150 147 log.error(e)
151 148 self.session.flash(e, queue='error')
152 149
153 return self._get_template_context()
150 return self._get_template_context(c)
154 151
155 152 @view_config(
156 153 route_name='login', request_method='POST',
157 154 renderer='rhodecode:templates/login.mako')
158 155 def login_post(self):
159 came_from = get_came_from(self.request)
156 c = self.load_default_context()
160 157
161 158 login_form = LoginForm()()
162 159
@@ -168,13 +165,13 b' class LoginView(object):'
168 165 self.session,
169 166 username=form_result['username'],
170 167 remember=form_result['remember'])
171 log.debug('Redirecting to "%s" after login.', came_from)
172 raise HTTPFound(came_from, headers=headers)
168 log.debug('Redirecting to "%s" after login.', c.came_from)
169 raise HTTPFound(c.came_from, headers=headers)
173 170 except formencode.Invalid as errors:
174 171 defaults = errors.value
175 172 # remove password from filling in form again
176 173 defaults.pop('password', None)
177 render_ctx = self._get_template_context()
174 render_ctx = self._get_template_context(c)
178 175 render_ctx.update({
179 176 'errors': errors.error_dict,
180 177 'defaults': defaults,
@@ -187,13 +184,13 b' class LoginView(object):'
187 184 # with user creation, explanation should be provided in
188 185 # Exception itself
189 186 self.session.flash(e, queue='error')
190 return self._get_template_context()
187 return self._get_template_context(c)
191 188
192 189 @CSRFRequired()
193 190 @view_config(route_name='logout', request_method='POST')
194 191 def logout(self):
195 user = self.request.user
196 log.info('Deleting session for user: `%s`', user)
192 auth_user = self._rhodecode_user
193 log.info('Deleting session for user: `%s`', auth_user)
197 194 self.session.delete()
198 195 return HTTPFound(url('home'))
199 196
@@ -203,6 +200,7 b' class LoginView(object):'
203 200 route_name='register', request_method='GET',
204 201 renderer='rhodecode:templates/register.mako',)
205 202 def register(self, defaults=None, errors=None):
203 c = self.load_default_context()
206 204 defaults = defaults or {}
207 205 errors = errors or {}
208 206
@@ -212,7 +210,7 b' class LoginView(object):'
212 210 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
213 211 .AuthUser.permissions['global']
214 212
215 render_ctx = self._get_template_context()
213 render_ctx = self._get_template_context(c)
216 214 render_ctx.update({
217 215 'defaults': defaults,
218 216 'errors': errors,
@@ -289,17 +287,24 b' class LoginView(object):'
289 287 'errors': {},
290 288 }
291 289
290 # always send implicit message to prevent from discovery of
291 # matching emails
292 msg = _('If such email exists, a password reset link was sent to it.')
293
292 294 if self.request.POST:
295 if h.HasPermissionAny('hg.password_reset.disabled')():
296 _email = self.request.POST.get('email', '')
297 log.error('Failed attempt to reset password for `%s`.', _email)
298 self.session.flash(_('Password reset has been disabled.'),
299 queue='error')
300 return HTTPFound(self.request.route_path('reset_password'))
301
293 302 password_reset_form = PasswordResetForm()()
294 303 try:
295 304 form_result = password_reset_form.to_python(
296 305 self.request.params)
297 if h.HasPermissionAny('hg.password_reset.disabled')():
298 log.error('Failed attempt to reset password for %s.', form_result['email'] )
299 self.session.flash(
300 _('Password reset has been disabled.'),
301 queue='error')
302 return HTTPFound(self.request.route_path('reset_password'))
306 user_email = form_result['email']
307
303 308 if captcha.active:
304 309 response = submit(
305 310 self.request.params.get('recaptcha_challenge_field'),
@@ -310,43 +315,76 b' class LoginView(object):'
310 315 _value = form_result
311 316 _msg = _('Bad captcha')
312 317 error_dict = {'recaptcha_field': _msg}
313 raise formencode.Invalid(_msg, _value, None,
314 error_dict=error_dict)
318 raise formencode.Invalid(
319 _msg, _value, None, error_dict=error_dict)
315 320
316 321 # Generate reset URL and send mail.
317 user_email = form_result['email']
318 322 user = User.get_by_email(user_email)
323
324 # generate password reset token that expires in 10minutes
325 desc = 'Generated token for password reset from {}'.format(
326 datetime.datetime.now().isoformat())
327 reset_token = AuthTokenModel().create(
328 user, lifetime=10,
329 description=desc,
330 role=UserApiKeys.ROLE_PASSWORD_RESET)
331 Session().commit()
332
333 log.debug('Successfully created password recovery token')
319 334 password_reset_url = self.request.route_url(
320 335 'reset_password_confirmation',
321 _query={'key': user.api_key})
336 _query={'key': reset_token.api_key})
322 337 UserModel().reset_password_link(
323 338 form_result, password_reset_url)
324
325 339 # Display success message and redirect.
326 self.session.flash(
327 _('Your password reset link was sent'),
328 queue='success')
329 return HTTPFound(self.request.route_path('login'))
340 self.session.flash(msg, queue='success')
341 return HTTPFound(self.request.route_path('reset_password'))
330 342
331 343 except formencode.Invalid as errors:
332 344 render_ctx.update({
333 345 'defaults': errors.value,
334 346 'errors': errors.error_dict,
335 347 })
348 if not self.request.params.get('email'):
349 # case of empty email, we want to report that
350 return render_ctx
351
352 if 'recaptcha_field' in errors.error_dict:
353 # case of failed captcha
354 return render_ctx
355
356 log.debug('faking response on invalid password reset')
357 # make this take 2s, to prevent brute forcing.
358 time.sleep(2)
359 self.session.flash(msg, queue='success')
360 return HTTPFound(self.request.route_path('reset_password'))
336 361
337 362 return render_ctx
338 363
339 364 @view_config(route_name='reset_password_confirmation',
340 365 request_method='GET')
341 366 def password_reset_confirmation(self):
367
342 368 if self.request.GET and self.request.GET.get('key'):
369 # make this take 2s, to prevent brute forcing.
370 time.sleep(2)
371
372 token = AuthTokenModel().get_auth_token(
373 self.request.GET.get('key'))
374
375 # verify token is the correct role
376 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
377 log.debug('Got token with role:%s expected is %s',
378 getattr(token, 'role', 'EMPTY_TOKEN'),
379 UserApiKeys.ROLE_PASSWORD_RESET)
380 self.session.flash(
381 _('Given reset token is invalid'), queue='error')
382 return HTTPFound(self.request.route_path('reset_password'))
383
343 384 try:
344 user = User.get_by_auth_token(self.request.GET.get('key'))
345 password_reset_url = self.request.route_url(
346 'reset_password_confirmation',
347 _query={'key': user.api_key})
348 data = {'email': user.email}
349 UserModel().reset_password(data, password_reset_url)
385 owner = token.user
386 data = {'email': owner.email, 'token': token.api_key}
387 UserModel().reset_password(data)
350 388 self.session.flash(
351 389 _('Your password reset was successful, '
352 390 'a new password has been sent to your email'),
1 NO CONTENT: file renamed from rhodecode/svn_support/__init__.py to rhodecode/apps/svn_support/__init__.py
1 NO CONTENT: file renamed from rhodecode/svn_support/config_keys.py to rhodecode/apps/svn_support/config_keys.py
1 NO CONTENT: file renamed from rhodecode/svn_support/events.py to rhodecode/apps/svn_support/events.py
1 NO CONTENT: file renamed from rhodecode/svn_support/subscribers.py to rhodecode/apps/svn_support/subscribers.py
1 NO CONTENT: file renamed from rhodecode/svn_support/templates/mod-dav-svn.conf.mako to rhodecode/apps/svn_support/templates/mod-dav-svn.conf.mako
@@ -25,7 +25,7 b' import re'
25 25
26 26 from pyramid import testing
27 27
28 from rhodecode.svn_support import utils
28 from rhodecode.apps.svn_support import utils
29 29
30 30
31 31 class TestModDavSvnConfig(object):
@@ -81,7 +81,7 b' def _render_mod_dav_svn_config('
81 81 }
82 82
83 83 # Render the configuration template to string.
84 template = 'rhodecode:svn_support/templates/mod-dav-svn.conf.mako'
84 template = 'rhodecode:apps/svn_support/templates/mod-dav-svn.conf.mako'
85 85 return render(template, context)
86 86
87 87
@@ -91,6 +91,11 b' class RhodeCodeAuthPluginBase(object):'
91 91 # set on authenticate() method and via set_auth_type func.
92 92 auth_type = None
93 93
94 # set on authenticate() method and via set_calling_scope_repo, this is a
95 # calling scope repository when doing authentication most likely on VCS
96 # operations
97 acl_repo_name = None
98
94 99 # List of setting names to store encrypted. Plugins may override this list
95 100 # to store settings encrypted.
96 101 _settings_encrypted = []
@@ -268,6 +273,9 b' class RhodeCodeAuthPluginBase(object):'
268 273 def set_auth_type(self, auth_type):
269 274 self.auth_type = auth_type
270 275
276 def set_calling_scope_repo(self, acl_repo_name):
277 self.acl_repo_name = acl_repo_name
278
271 279 def allows_authentication_from(
272 280 self, user, allows_non_existing_user=True,
273 281 allowed_auth_plugins=None, allowed_auth_sources=None):
@@ -332,6 +340,8 b' class RhodeCodeAuthPluginBase(object):'
332 340 log.debug('provided username:`%s` is empty skipping...', username)
333 341 if not user:
334 342 log.debug('User `%s` not found in database', username)
343 else:
344 log.debug('Got DB user:%s', user)
335 345 return user
336 346
337 347 def user_activation_state(self):
@@ -518,7 +528,7 b' def get_auth_cache_manager(custom_ttl=No'
518 528
519 529
520 530 def authenticate(username, password, environ=None, auth_type=None,
521 skip_missing=False, registry=None):
531 skip_missing=False, registry=None, acl_repo_name=None):
522 532 """
523 533 Authentication function used for access control,
524 534 It tries to authenticate based on enabled authentication modules.
@@ -538,6 +548,7 b' def authenticate(username, password, env'
538 548 authn_registry = get_authn_registry(registry)
539 549 for plugin in authn_registry.get_plugins_for_authentication():
540 550 plugin.set_auth_type(auth_type)
551 plugin.set_calling_scope_repo(acl_repo_name)
541 552 user = plugin.get_user(username)
542 553 display_user = user.username if user else username
543 554
@@ -627,3 +638,21 b' def authenticate(username, password, env'
627 638 log.debug("User `%s` failed to authenticate against %s",
628 639 display_user, plugin.get_id())
629 640 return None
641
642
643 def chop_at(s, sub, inclusive=False):
644 """Truncate string ``s`` at the first occurrence of ``sub``.
645
646 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
647
648 >>> chop_at("plutocratic brats", "rat")
649 'plutoc'
650 >>> chop_at("plutocratic brats", "rat", True)
651 'plutocrat'
652 """
653 pos = s.find(sub)
654 if pos == -1:
655 return s
656 if inclusive:
657 return s[:pos+len(sub)]
658 return s[:pos]
@@ -28,10 +28,9 b' import base64'
28 28 import logging
29 29 import urllib2
30 30
31 from pylons.i18n.translation import lazy_ugettext as _
32 from sqlalchemy.ext.hybrid import hybrid_property
33
34 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
35 34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 36 from rhodecode.lib.colander_utils import strip_whitespace
@@ -21,15 +21,14 b''
21 21 import colander
22 22 import logging
23 23
24 from sqlalchemy.ext.hybrid import hybrid_property
25
26 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
24 from rhodecode.translation import _
25 from rhodecode.authentication.base import (
26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 29 from rhodecode.lib.colander_utils import strip_whitespace
30 30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 31 from rhodecode.model.db import User
32 from rhodecode.translation import _
33 32
34 33
35 34 log = logging.getLogger(__name__)
@@ -30,10 +30,9 b' import rhodecode'
30 30 import urllib
31 31 import urllib2
32 32
33 from pylons.i18n.translation import lazy_ugettext as _
34 from sqlalchemy.ext.hybrid import hybrid_property
35
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
37 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
39 38 from rhodecode.lib.colander_utils import strip_whitespace
@@ -27,10 +27,9 b' import colander'
27 27 import logging
28 28 import traceback
29 29
30 from pylons.i18n.translation import lazy_ugettext as _
31 from sqlalchemy.ext.hybrid import hybrid_property
32
33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
30 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
34 33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 34 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 35 from rhodecode.lib.colander_utils import strip_whitespace
@@ -72,14 +71,15 b' class LdapSettingsSchema(AuthnPluginSett'
72 71 host = colander.SchemaNode(
73 72 colander.String(),
74 73 default='',
75 description=_('Host of the LDAP Server'),
74 description=_('Host of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com'),
76 76 preparer=strip_whitespace,
77 77 title=_('LDAP Host'),
78 78 widget='string')
79 79 port = colander.SchemaNode(
80 80 colander.Int(),
81 81 default=389,
82 description=_('Port that the LDAP server is listening on'),
82 description=_('Custom port that the LDAP server is listening on. Default: 389'),
83 83 preparer=strip_whitespace,
84 84 title=_('Port'),
85 85 validator=colander.Range(min=0, max=65536),
@@ -87,7 +87,9 b' class LdapSettingsSchema(AuthnPluginSett'
87 87 dn_user = colander.SchemaNode(
88 88 colander.String(),
89 89 default='',
90 description=_('User to connect to LDAP'),
90 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
91 'e.g., cn=admin,dc=mydomain,dc=com, or '
92 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
91 93 missing='',
92 94 preparer=strip_whitespace,
93 95 title=_('Account'),
@@ -95,7 +97,7 b' class LdapSettingsSchema(AuthnPluginSett'
95 97 dn_pass = colander.SchemaNode(
96 98 colander.String(),
97 99 default='',
98 description=_('Password to connect to LDAP'),
100 description=_('Password to authenticate for given user DN.'),
99 101 missing='',
100 102 preparer=strip_whitespace,
101 103 title=_('Password'),
@@ -117,7 +119,9 b' class LdapSettingsSchema(AuthnPluginSett'
117 119 base_dn = colander.SchemaNode(
118 120 colander.String(),
119 121 default='',
120 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
122 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
123 'in it to be replaced with current user credentials \n'
124 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
121 125 missing='',
122 126 preparer=strip_whitespace,
123 127 title=_('Base DN'),
@@ -125,22 +129,25 b' class LdapSettingsSchema(AuthnPluginSett'
125 129 filter = colander.SchemaNode(
126 130 colander.String(),
127 131 default='',
128 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
132 description=_('Filter to narrow results \n'
133 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
134 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
129 135 missing='',
130 136 preparer=strip_whitespace,
131 137 title=_('LDAP Search Filter'),
132 138 widget='string')
139
133 140 search_scope = colander.SchemaNode(
134 141 colander.String(),
135 default=search_scope_choices[0],
136 description=_('How deep to search LDAP'),
142 default=search_scope_choices[2],
143 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
137 144 title=_('LDAP Search Scope'),
138 145 validator=colander.OneOf(search_scope_choices),
139 146 widget='select')
140 147 attr_login = colander.SchemaNode(
141 148 colander.String(),
142 default='',
143 description=_('LDAP Attribute to map to user name'),
149 default='uid',
150 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
144 151 preparer=strip_whitespace,
145 152 title=_('Login Attribute'),
146 153 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
@@ -148,7 +155,7 b' class LdapSettingsSchema(AuthnPluginSett'
148 155 attr_firstname = colander.SchemaNode(
149 156 colander.String(),
150 157 default='',
151 description=_('LDAP Attribute to map to first name'),
158 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
152 159 missing='',
153 160 preparer=strip_whitespace,
154 161 title=_('First Name Attribute'),
@@ -156,7 +163,7 b' class LdapSettingsSchema(AuthnPluginSett'
156 163 attr_lastname = colander.SchemaNode(
157 164 colander.String(),
158 165 default='',
159 description=_('LDAP Attribute to map to last name'),
166 description=_('LDAP Attribute to map to last name (e.g., sn)'),
160 167 missing='',
161 168 preparer=strip_whitespace,
162 169 title=_('Last Name Attribute'),
@@ -164,7 +171,9 b' class LdapSettingsSchema(AuthnPluginSett'
164 171 attr_email = colander.SchemaNode(
165 172 colander.String(),
166 173 default='',
167 description=_('LDAP Attribute to map to email address'),
174 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
175 'Emails are a crucial part of RhodeCode. \n'
176 'If possible add a valid email attribute to ldap users.'),
168 177 missing='',
169 178 preparer=strip_whitespace,
170 179 title=_('Email Attribute'),
@@ -182,7 +191,7 b' class AuthLdap(object):'
182 191 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
183 192 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
184 193 search_scope='SUBTREE', attr_login='uid',
185 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
194 ldap_filter=None):
186 195 if ldap == Missing:
187 196 raise LdapImportError("Missing or incompatible ldap library")
188 197
@@ -236,14 +245,13 b' class AuthLdap(object):'
236 245 server.start_tls_s()
237 246
238 247 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
239 log.debug('Trying simple_bind with password and given DN: %s',
248 log.debug('Trying simple_bind with password and given login DN: %s',
240 249 self.LDAP_BIND_DN)
241 250 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
242 251
243 252 return server
244 253
245 254 def get_uid(self, username):
246 from rhodecode.lib.helpers import chop_at
247 255 uid = username
248 256 for server_addr in self.SERVER_ADDRESSES:
249 257 uid = chop_at(username, "@%s" % server_addr)
@@ -292,8 +300,11 b' class AuthLdap(object):'
292 300 self.BASE_DN, self.SEARCH_SCOPE, filter_)
293 301
294 302 if not lobjects:
303 log.debug("No matching LDAP objects for authentication "
304 "of UID:'%s' username:(%s)", uid, username)
295 305 raise ldap.NO_SUCH_OBJECT()
296 306
307 log.debug('Found matching ldap object, trying to authenticate')
297 308 for (dn, _attrs) in lobjects:
298 309 if dn is None:
299 310 continue
@@ -304,15 +315,13 b' class AuthLdap(object):'
304 315 break
305 316
306 317 else:
307 log.debug("No matching LDAP objects for authentication "
308 "of '%s' (%s)", uid, username)
309 318 raise LdapPasswordError('Failed to authenticate user '
310 319 'with given password')
311 320
312 321 except ldap.NO_SUCH_OBJECT:
313 322 log.debug("LDAP says no such user '%s' (%s), org_exc:",
314 323 uid, username, exc_info=True)
315 raise LdapUsernameError()
324 raise LdapUsernameError('Unable to find user')
316 325 except ldap.SERVER_DOWN:
317 326 org_exc = traceback.format_exc()
318 327 raise LdapConnectionError(
@@ -447,7 +456,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
447 456 'email': get_ldap_attr('attr_email') or email,
448 457 'admin': admin,
449 458 'active': active,
450 "active_from_extern": None,
459 'active_from_extern': None,
451 460 'extern_name': user_dn,
452 461 'extern_type': extern_type,
453 462 }
@@ -17,6 +17,7 b''
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 RhodeCode authentication library for PAM
22 23 """
@@ -29,10 +30,9 b' import pwd'
29 30 import re
30 31 import socket
31 32
32 from pylons.i18n.translation import lazy_ugettext as _
33 from sqlalchemy.ext.hybrid import hybrid_property
34
35 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
@@ -25,9 +25,8 b' RhodeCode authentication plugin for buil'
25 25 import logging
26 26
27 27 from pylons.i18n.translation import lazy_ugettext as _
28 from sqlalchemy.ext.hybrid import hybrid_property
29 28
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
31 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 31 from rhodecode.lib.utils2 import safe_str
33 32 from rhodecode.model.db import User
@@ -24,12 +24,11 b' RhodeCode authentication token plugin fo'
24 24
25 25 import logging
26 26
27 from sqlalchemy.ext.hybrid import hybrid_property
28
29 27 from rhodecode.translation import _
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
31 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.model.db import User, UserApiKeys
31 from rhodecode.model.db import User, UserApiKeys, Repository
33 32
34 33
35 34 log = logging.getLogger(__name__)
@@ -122,10 +121,17 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
122 121
123 122 log.debug('Authenticating user with args %s', user_attrs)
124 123 if userobj.active:
125 role = UserApiKeys.ROLE_VCS
126 active_tokens = [x.api_key for x in
127 User.extra_valid_auth_tokens(userobj, role=role)]
128 if userobj.username == username and password in active_tokens:
124 # calling context repo for token scopes
125 scope_repo_id = None
126 if self.acl_repo_name:
127 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 scope_repo_id = repo.repo_id if repo else None
129
130 token_match = userobj.authenticate_by_token(
131 password, roles=[UserApiKeys.ROLE_VCS],
132 scope_repo_id=scope_repo_id)
133
134 if userobj.username == username and token_match:
129 135 log.info(
130 136 'user `%s` successfully authenticated via %s',
131 137 user_attrs['username'], self.name)
@@ -40,10 +40,11 b' class AuthnPluginSettingsSchemaBase(cola'
40 40 cache_ttl = colander.SchemaNode(
41 41 colander.Int(),
42 42 default=0,
43 description=_('Amount of seconds to cache the authentication '
44 'call for this plugin. Useful for long calls like '
45 'LDAP to improve the responsiveness of the '
46 'authentication system (0 means disabled).'),
43 description=_('Amount of seconds to cache the authentication response'
44 'call for this plugin. \n'
45 'Useful for long calls like LDAP to improve the '
46 'performance of the authentication system '
47 '(0 means disabled).'),
47 48 missing=0,
48 49 title=_('Auth Cache TTL'),
49 50 validator=colander.Range(min=0, max=None),
@@ -87,15 +87,6 b' def load_environment(global_conf, app_co'
87 87
88 88 config['routes.map'] = make_map(config)
89 89
90 if asbool(config.get('generate_js_files', 'false')):
91 jsroutes = config['routes.map'].jsroutes()
92 jsroutes_file_content = generate_jsroutes_content(jsroutes)
93 jsroutes_file_path = os.path.join(
94 paths['static_files'], 'js', 'rhodecode', 'routes.js')
95
96 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
97 f.write(jsroutes_file_content)
98
99 90 config['pylons.app_globals'] = app_globals.Globals(config)
100 91 config['pylons.h'] = helpers
101 92 rhodecode.CONFIG = config
@@ -184,7 +175,6 b' def load_pyramid_environment(global_conf'
184 175 protocol=utils.get_vcs_server_protocol(settings),
185 176 log_level=settings['vcs.server.log_level'])
186 177
187 utils.configure_pyro4(settings)
188 178 utils.configure_vcs(settings)
189 179 if vcs_server_enabled:
190 180 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
@@ -40,10 +40,7 b''
40 40 },
41 41 "python2.7-Pylons-1.0.1-patch1": {
42 42 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
43 },
44 "python2.7-Pyro4-4.35": {
45 "MIT License": "http://spdx.org/licenses/MIT"
46 },
43 },
47 44 "python2.7-Routes-1.13": {
48 45 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
49 46 },
@@ -214,10 +211,7 b''
214 211 },
215 212 "python2.7-requests-2.9.1": {
216 213 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
217 },
218 "python2.7-serpent-1.12": {
219 "MIT License": "http://spdx.org/licenses/MIT"
220 },
214 },
221 215 "python2.7-setuptools-19.4": {
222 216 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0",
223 217 "Zope Public License 2.0": "http://spdx.org/licenses/ZPL-2.0"
@@ -32,7 +32,7 b' from pyramid.config import Configurator'
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import (
35 HTTPError, HTTPInternalServerError, HTTPFound)
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 36 from pyramid.events import ApplicationCreated
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
@@ -53,7 +53,8 b' from rhodecode.lib.middleware.vcs import'
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import (
56 scan_repositories_if_enabled, write_metadata_if_needed)
56 scan_repositories_if_enabled, write_metadata_if_needed,
57 write_js_routes_if_enabled)
57 58
58 59
59 60 log = logging.getLogger(__name__)
@@ -219,18 +220,14 b' def add_pylons_compat_data(registry, glo'
219 220
220 221
221 222 def error_handler(exception, request):
222 from rhodecode.model.settings import SettingsModel
223 import rhodecode
223 224 from rhodecode.lib.utils2 import AttributeDict
224 225
225 try:
226 rc_config = SettingsModel().get_all_settings()
227 except Exception:
228 log.exception('failed to fetch settings')
229 rc_config = {}
226 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
230 227
231 228 base_response = HTTPInternalServerError()
232 229 # prefer original exception for the response since it may have headers set
233 if isinstance(exception, HTTPError):
230 if isinstance(exception, HTTPException):
234 231 base_response = exception
235 232
236 233 def is_http_error(response):
@@ -251,7 +248,7 b' def error_handler(exception, request):'
251 248 request.route_url('rhodecode_support')
252 249 )
253 250 c.redirect_time = 0
254 c.rhodecode_name = rc_config.get('rhodecode_title', '')
251 c.rhodecode_name = rhodecode_title
255 252 if not c.rhodecode_name:
256 253 c.rhodecode_name = 'Rhodecode'
257 254
@@ -281,14 +278,24 b' def includeme(config):'
281 278 # Includes which are required. The application would fail without them.
282 279 config.include('pyramid_mako')
283 280 config.include('pyramid_beaker')
284 config.include('rhodecode.channelstream')
285 config.include('rhodecode.admin')
281
286 282 config.include('rhodecode.authentication')
287 283 config.include('rhodecode.integrations')
288 config.include('rhodecode.login')
284
285 # apps
286 config.include('rhodecode.apps._base')
287
288 config.include('rhodecode.apps.admin')
289 config.include('rhodecode.apps.channelstream')
290 config.include('rhodecode.apps.login')
291 config.include('rhodecode.apps.repository')
292 config.include('rhodecode.apps.user_profile')
293 config.include('rhodecode.apps.my_account')
294 config.include('rhodecode.apps.svn_support')
295
289 296 config.include('rhodecode.tweens')
290 297 config.include('rhodecode.api')
291 config.include('rhodecode.svn_support')
298
292 299 config.add_route(
293 300 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
294 301
@@ -298,6 +305,7 b' def includeme(config):'
298 305 # Add subscribers.
299 306 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
300 307 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
308 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
301 309
302 310 # Set the authorization policy.
303 311 authz_policy = ACLAuthorizationPolicy()
@@ -92,7 +92,7 b' class JSRoutesMapper(Mapper):'
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 ('user_profile', '/profile/%(username)s', ['username'])
95 ('show_user', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
@@ -198,10 +198,6 b' def make_map(config):'
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 rmap.connect(
202 'user_profile', '/_profiles/{username}', controller='users',
203 action='user_profile')
204
205 201 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 202 rmap.connect('rst_help',
207 203 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
@@ -296,8 +292,6 b' def make_map(config):'
296 292 controller='admin/users') as m:
297 293 m.connect('users', '/users',
298 294 action='create', conditions={'method': ['POST']})
299 m.connect('users', '/users',
300 action='index', conditions={'method': ['GET']})
301 295 m.connect('new_user', '/users/new',
302 296 action='new', conditions={'method': ['GET']})
303 297 m.connect('update_user', '/users/{user_id}',
@@ -319,13 +313,6 b' def make_map(config):'
319 313 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 314 action='update_advanced', conditions={'method': ['PUT']})
321 315
322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 action='add_auth_token', conditions={'method': ['PUT']})
326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 action='delete_auth_token', conditions={'method': ['DELETE']})
328
329 316 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 317 action='edit_global_perms', conditions={'method': ['GET']})
331 318 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
@@ -387,6 +374,10 b' def make_map(config):'
387 374 '/user_groups/{user_group_id}/edit/advanced',
388 375 action='edit_advanced', conditions={'method': ['GET']})
389 376
377 m.connect('edit_user_group_advanced_sync',
378 '/user_groups/{user_group_id}/edit/advanced/sync',
379 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
380
390 381 m.connect('edit_user_group_members',
391 382 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 383 action='user_group_members', conditions={'method': ['GET']})
@@ -518,15 +509,15 b' def make_map(config):'
518 509 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 510 controller='admin/my_account') as m:
520 511
521 m.connect('my_account', '/my_account',
522 action='my_account', conditions={'method': ['GET']})
523 512 m.connect('my_account_edit', '/my_account/edit',
524 513 action='my_account_edit', conditions={'method': ['GET']})
525 m.connect('my_account', '/my_account',
514 m.connect('my_account', '/my_account/update',
526 515 action='my_account_update', conditions={'method': ['POST']})
527 516
517 # NOTE(marcink): this needs to be kept for password force flag to be
518 # handler, remove after migration to pyramid
528 519 m.connect('my_account_password', '/my_account/password',
529 action='my_account_password', conditions={'method': ['GET', 'POST']})
520 action='my_account_password', conditions={'method': ['GET']})
530 521
531 522 m.connect('my_account_repos', '/my_account/repos',
532 523 action='my_account_repos', conditions={'method': ['GET']})
@@ -547,12 +538,6 b' def make_map(config):'
547 538 m.connect('my_account_emails', '/my_account/emails',
548 539 action='my_account_emails_delete', conditions={'method': ['DELETE']})
549 540
550 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
551 action='my_account_auth_tokens', conditions={'method': ['GET']})
552 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
553 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
554 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
555 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
556 541 m.connect('my_account_notifications', '/my_account/notifications',
557 542 action='my_notifications',
558 543 conditions={'method': ['GET']})
@@ -1066,6 +1051,12 b' def make_map(config):'
1066 1051 f_path='', annotate=True, conditions={'function': check_repo},
1067 1052 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1068 1053
1054 rmap.connect('files_annotate_previous',
1055 '/{repo_name}/annotate-previous/{revision}/{f_path}',
1056 controller='files', action='annotate_previous', revision='tip',
1057 f_path='', annotate=True, conditions={'function': check_repo},
1058 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1059
1069 1060 rmap.connect('files_edit',
1070 1061 '/{repo_name}/edit/{revision}/{f_path}',
1071 1062 controller='files', action='edit', revision='tip',
@@ -1137,11 +1128,6 b' def make_map(config):'
1137 1128 conditions={'function': check_repo},
1138 1129 requirements=URL_NAME_REQUIREMENTS)
1139 1130
1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1141 controller='followers', action='followers',
1142 conditions={'function': check_repo},
1143 requirements=URL_NAME_REQUIREMENTS)
1144
1145 1131 # must be here for proper group/repo catching pattern
1146 1132 _connect_with_slash(
1147 1133 rmap, 'repo_group_home', '/{group_name}',
@@ -20,29 +20,11 b''
20 20
21 21 import os
22 22 import shlex
23 import Pyro4
24 23 import platform
25 24
26 25 from rhodecode.model import init_model
27 26
28 27
29 def configure_pyro4(config):
30 """
31 Configure Pyro4 based on `config`.
32
33 This will mainly set the different configuration parameters of the Pyro4
34 library based on the settings in our INI files. The Pyro4 documentation
35 lists more details about the specific settings and their meaning.
36 """
37 Pyro4.config.COMMTIMEOUT = float(config['vcs.connection_timeout'])
38 Pyro4.config.SERIALIZER = 'pickle'
39 Pyro4.config.SERIALIZERS_ACCEPTED.add('pickle')
40
41 # Note: We need server configuration in the WSGI processes
42 # because we provide a callback server in certain vcs operations.
43 Pyro4.config.SERVERTYPE = "multiplex"
44 Pyro4.config.POLLTIMEOUT = 0.01
45
46 28
47 29 def configure_vcs(config):
48 30 """
@@ -77,9 +59,16 b' def initialize_test_environment(settings'
77 59 create_test_directory, create_test_database, create_test_repositories,
78 60 create_test_index)
79 61 from rhodecode.tests import TESTS_TMP_PATH
62 from rhodecode.lib.vcs.backends.hg import largefiles_store
63 from rhodecode.lib.vcs.backends.git import lfs_store
64
80 65 # test repos
81 66 if test_env:
82 67 create_test_directory(TESTS_TMP_PATH)
68 # large object stores
69 create_test_directory(largefiles_store(TESTS_TMP_PATH))
70 create_test_directory(lfs_store(TESTS_TMP_PATH))
71
83 72 create_test_database(TESTS_TMP_PATH, settings)
84 73 create_test_repositories(TESTS_TMP_PATH, settings)
85 74 create_test_index(TESTS_TMP_PATH, settings)
@@ -28,101 +28,17 b' import logging'
28 28 from pylons import request, tmpl_context as c, url
29 29 from pylons.controllers.util import redirect
30 30 from sqlalchemy.orm import joinedload
31 from whoosh.qparser.default import QueryParser, query
32 from whoosh.qparser.dateparse import DateParserPlugin
33 from whoosh.fields import (TEXT, Schema, DATETIME)
34 from sqlalchemy.sql.expression import or_, and_, func
35 31
36 32 from rhodecode.model.db import UserLog, PullRequest
33 from rhodecode.lib.user_log_filter import user_log_filter
37 34 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 35 from rhodecode.lib.base import BaseController, render
39 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
36 from rhodecode.lib.utils2 import safe_int
40 37 from rhodecode.lib.helpers import Page
41 38
42 39
43 40 log = logging.getLogger(__name__)
44 41
45 # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh
46 # querylang to build sql queries and filter journals
47 JOURNAL_SCHEMA = Schema(
48 username=TEXT(),
49 date=DATETIME(),
50 action=TEXT(),
51 repository=TEXT(),
52 ip=TEXT(),
53 )
54
55
56 def _journal_filter(user_log, search_term):
57 """
58 Filters sqlalchemy user_log based on search_term with whoosh Query language
59 http://packages.python.org/Whoosh/querylang.html
60
61 :param user_log:
62 :param search_term:
63 """
64 log.debug('Initial search term: %r' % search_term)
65 qry = None
66 if search_term:
67 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
68 qp.add_plugin(DateParserPlugin())
69 qry = qp.parse(unicode(search_term))
70 log.debug('Filtering using parsed query %r' % qry)
71
72 def wildcard_handler(col, wc_term):
73 if wc_term.startswith('*') and not wc_term.endswith('*'):
74 # postfix == endswith
75 wc_term = remove_prefix(wc_term, prefix='*')
76 return func.lower(col).endswith(wc_term)
77 elif wc_term.startswith('*') and wc_term.endswith('*'):
78 # wildcard == ilike
79 wc_term = remove_prefix(wc_term, prefix='*')
80 wc_term = remove_suffix(wc_term, suffix='*')
81 return func.lower(col).contains(wc_term)
82
83 def get_filterion(field, val, term):
84
85 if field == 'repository':
86 field = getattr(UserLog, 'repository_name')
87 elif field == 'ip':
88 field = getattr(UserLog, 'user_ip')
89 elif field == 'date':
90 field = getattr(UserLog, 'action_date')
91 elif field == 'username':
92 field = getattr(UserLog, 'username')
93 else:
94 field = getattr(UserLog, field)
95 log.debug('filter field: %s val=>%s' % (field, val))
96
97 # sql filtering
98 if isinstance(term, query.Wildcard):
99 return wildcard_handler(field, val)
100 elif isinstance(term, query.Prefix):
101 return func.lower(field).startswith(func.lower(val))
102 elif isinstance(term, query.DateRange):
103 return and_(field >= val[0], field <= val[1])
104 return func.lower(field) == func.lower(val)
105
106 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
107 query.DateRange)):
108 if not isinstance(qry, query.And):
109 qry = [qry]
110 for term in qry:
111 field = term.fieldname
112 val = (term.text if not isinstance(term, query.DateRange)
113 else [term.startdate, term.enddate])
114 user_log = user_log.filter(get_filterion(field, val, term))
115 elif isinstance(qry, query.Or):
116 filters = []
117 for term in qry:
118 field = term.fieldname
119 val = (term.text if not isinstance(term, query.DateRange)
120 else [term.startdate, term.enddate])
121 filters.append(get_filterion(field, val, term))
122 user_log = user_log.filter(or_(*filters))
123
124 return user_log
125
126 42
127 43 class AdminController(BaseController):
128 44
@@ -139,7 +55,7 b' class AdminController(BaseController):'
139 55 # FILTERING
140 56 c.search_term = request.GET.get('filter')
141 57 try:
142 users_log = _journal_filter(users_log, c.search_term)
58 users_log = user_log_filter(users_log, c.search_term)
143 59 except Exception:
144 60 # we want this to crash for now
145 61 raise
@@ -29,32 +29,30 b' import datetime'
29 29 import formencode
30 30 from formencode import htmlfill
31 31 from pyramid.threadlocal import get_current_registry
32 from pylons import request, tmpl_context as c, url, session
32 from pyramid.httpexceptions import HTTPFound
33
34 from pylons import request, tmpl_context as c, url
33 35 from pylons.controllers.util import redirect
34 36 from pylons.i18n.translation import _
35 37 from sqlalchemy.orm import joinedload
36 from webob.exc import HTTPBadGateway
37 38
38 from rhodecode import forms
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib import auth
41 41 from rhodecode.lib.auth import (
42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
42 LoginRequired, NotAnonymous, AuthUser)
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils import jsonify
45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
45 from rhodecode.lib.utils2 import safe_int, str2bool
46 46 from rhodecode.lib.ext_json import json
47 47 from rhodecode.lib.channelstream import channelstream_request, \
48 48 ChannelstreamException
49 49
50 from rhodecode.model.validation_schema.schemas import user_schema
51 50 from rhodecode.model.db import (
52 51 Repository, PullRequest, UserEmailMap, User, UserFollowing)
53 52 from rhodecode.model.forms import UserForm
54 53 from rhodecode.model.scm import RepoList
55 54 from rhodecode.model.user import UserModel
56 55 from rhodecode.model.repo import RepoModel
57 from rhodecode.model.auth_token import AuthTokenModel
58 56 from rhodecode.model.meta import Session
59 57 from rhodecode.model.pull_request import PullRequestModel
60 58 from rhodecode.model.comment import CommentsModel
@@ -79,7 +77,7 b' class MyAccountController(BaseController'
79 77 if c.user.username == User.DEFAULT_USER:
80 78 h.flash(_("You can't edit this user since it's"
81 79 " crucial for entire application"), category='warning')
82 return redirect(url('users'))
80 return redirect(h.route_path('users'))
83 81
84 82 c.auth_user = AuthUser(
85 83 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
@@ -156,7 +154,7 b' class MyAccountController(BaseController'
156 154 % form_result.get('username'), category='error')
157 155
158 156 if update:
159 return redirect('my_account')
157 raise HTTPFound(h.route_path('my_account_profile'))
160 158
161 159 return htmlfill.render(
162 160 render('admin/my_account/my_account.mako'),
@@ -165,19 +163,6 b' class MyAccountController(BaseController'
165 163 force_defaults=False
166 164 )
167 165
168 def my_account(self):
169 """
170 GET /_admin/my_account Displays info about my account
171 """
172 # url('my_account')
173 c.active = 'profile'
174 self.__load_data()
175
176 defaults = c.user.get_dict()
177 return htmlfill.render(
178 render('admin/my_account/my_account.mako'),
179 defaults=defaults, encoding="UTF-8", force_defaults=False)
180
181 166 def my_account_edit(self):
182 167 """
183 168 GET /_admin/my_account/edit Displays edit form of my account
@@ -196,47 +181,6 b' class MyAccountController(BaseController'
196 181 force_defaults=False
197 182 )
198 183
199 @auth.CSRFRequired(except_methods=['GET'])
200 def my_account_password(self):
201 c.active = 'password'
202 self.__load_data()
203 c.extern_type = c.user.extern_type
204
205 schema = user_schema.ChangePasswordSchema().bind(
206 username=c.rhodecode_user.username)
207
208 form = forms.Form(schema,
209 buttons=(forms.buttons.save, forms.buttons.reset))
210
211 if request.method == 'POST' and c.extern_type == 'rhodecode':
212 controls = request.POST.items()
213 try:
214 valid_data = form.validate(controls)
215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
216 instance = c.rhodecode_user.get_instance()
217 instance.update_userdata(force_password_change=False)
218 Session().commit()
219 except forms.ValidationFailure as e:
220 request.session.flash(
221 _('Error occurred during update of user password'),
222 queue='error')
223 form = e
224 except Exception:
225 log.exception("Exception updating password")
226 request.session.flash(
227 _('Error occurred during update of user password'),
228 queue='error')
229 else:
230 session.setdefault('rhodecode_user', {}).update(
231 {'password': md5(instance.password)})
232 session.save()
233 request.session.flash(
234 _("Successfully updated password"), queue='success')
235 return redirect(url('my_account_password'))
236
237 c.form = form
238 return render('admin/my_account/my_account.mako')
239
240 184 def my_account_repos(self):
241 185 c.active = 'repos'
242 186 self.__load_data()
@@ -376,54 +320,6 b' class MyAccountController(BaseController'
376 320 else:
377 321 return json.dumps(data)
378 322
379 def my_account_auth_tokens(self):
380 c.active = 'auth_tokens'
381 self.__load_data()
382 show_expired = True
383 c.lifetime_values = [
384 (str(-1), _('forever')),
385 (str(5), _('5 minutes')),
386 (str(60), _('1 hour')),
387 (str(60 * 24), _('1 day')),
388 (str(60 * 24 * 30), _('1 month')),
389 ]
390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
392 for x in AuthTokenModel.cls.ROLES]
393 c.role_options = [(c.role_values, _("Role"))]
394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
395 c.rhodecode_user.user_id, show_expired=show_expired)
396 return render('admin/my_account/my_account.mako')
397
398 @auth.CSRFRequired()
399 def my_account_auth_tokens_add(self):
400 lifetime = safe_int(request.POST.get('lifetime'), -1)
401 description = request.POST.get('description')
402 role = request.POST.get('role')
403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
404 role)
405 Session().commit()
406 h.flash(_("Auth token successfully created"), category='success')
407 return redirect(url('my_account_auth_tokens'))
408
409 @auth.CSRFRequired()
410 def my_account_auth_tokens_delete(self):
411 auth_token = request.POST.get('del_auth_token')
412 user_id = c.rhodecode_user.user_id
413 if request.POST.get('del_auth_token_builtin'):
414 user = User.get(user_id)
415 if user:
416 user.api_key = generate_auth_token(user.username)
417 Session().add(user)
418 Session().commit()
419 h.flash(_("Auth token successfully reset"), category='success')
420 elif auth_token:
421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
422 Session().commit()
423 h.flash(_("Auth token successfully deleted"), category='success')
424
425 return redirect(url('my_account_auth_tokens'))
426
427 323 def my_notifications(self):
428 324 c.active = 'notifications'
429 325 return render('admin/my_account/my_account.mako')
@@ -36,7 +36,8 b' from pyramid.threadlocal import get_curr'
36 36 from webob.exc import HTTPBadRequest
37 37
38 38 import rhodecode
39 from rhodecode.admin.navigation import navigation_list
39 from rhodecode.apps.admin.navigation import navigation_list
40 from rhodecode.apps.svn_support.config_keys import generate_config
40 41 from rhodecode.lib import auth
41 42 from rhodecode.lib import helpers as h
42 43 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
@@ -62,7 +63,6 b' from rhodecode.model.settings import ('
62 63 SettingsModel)
63 64
64 65 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
65 from rhodecode.svn_support.config_keys import generate_config
66 66
67 67
68 68 log = logging.getLogger(__name__)
@@ -82,7 +82,7 b' class SettingsController(BaseController)'
82 82 rhodecode.CONFIG.get('labs_settings_active', 'true'))
83 83 c.navlist = navigation_list(request)
84 84
85 def _get_hg_ui_settings(self):
85 def _get_ui_settings(self):
86 86 ret = RhodeCodeUi.query().all()
87 87
88 88 if not ret:
@@ -94,7 +94,7 b' class SettingsController(BaseController)'
94 94 if k == '/':
95 95 k = 'root_path'
96 96
97 if k in ['push_ssl', 'publish']:
97 if k in ['push_ssl', 'publish', 'enabled']:
98 98 v = str2bool(v)
99 99
100 100 if k.find('.') != -1:
@@ -165,6 +165,7 b' class SettingsController(BaseController)'
165 165
166 166 model.create_or_update_global_svn_settings(form_result)
167 167 model.create_or_update_global_hg_settings(form_result)
168 model.create_or_update_global_git_settings(form_result)
168 169 model.create_or_update_global_pr_settings(form_result)
169 170 except Exception:
170 171 log.exception("Exception while updating settings")
@@ -668,7 +669,8 b' class SettingsController(BaseController)'
668 669
669 670 def _form_defaults(self):
670 671 defaults = SettingsModel().get_all_settings()
671 defaults.update(self._get_hg_ui_settings())
672 defaults.update(self._get_ui_settings())
673
672 674 defaults.update({
673 675 'new_svn_branch': '',
674 676 'new_svn_tag': '',
@@ -117,7 +117,7 b' class UserGroupsController(BaseControlle'
117 117 def user_group_actions(user_group_id, user_group_name):
118 118 return _render("user_group_actions", user_group_id, user_group_name)
119 119
120 ## json generate
120 # json generate
121 121 group_iter = UserGroupList(UserGroup.query().all(),
122 122 perm_set=['usergroup.admin'])
123 123
@@ -129,6 +129,7 b' class UserGroupsController(BaseControlle'
129 129 "group_name_raw": user_gr.users_group_name,
130 130 "desc": h.escape(user_gr.user_group_description),
131 131 "members": len(user_gr.members),
132 "sync": user_gr.group_data.get('extern_type'),
132 133 "active": h.bool2icon(user_gr.users_group_active),
133 134 "owner": h.escape(h.link_to_user(user_gr.user.username)),
134 135 "action": user_group_actions(
@@ -431,7 +432,6 b' class UserGroupsController(BaseControlle'
431 432 prefix_error=False,
432 433 encoding="UTF-8",
433 434 force_defaults=False)
434
435 435 except Exception:
436 436 log.exception("Exception during permissions saving")
437 437 h.flash(_('An error occurred during permissions saving'),
@@ -459,6 +459,36 b' class UserGroupsController(BaseControlle'
459 459 return render('admin/user_groups/user_group_edit.mako')
460 460
461 461 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
462 def edit_advanced_set_synchronization(self, user_group_id):
463 user_group_id = safe_int(user_group_id)
464 user_group = UserGroup.get_or_404(user_group_id)
465
466 existing = user_group.group_data.get('extern_type')
467
468 if existing:
469 new_state = user_group.group_data
470 new_state['extern_type'] = None
471 else:
472 new_state = user_group.group_data
473 new_state['extern_type'] = 'manual'
474 new_state['extern_type_set_by'] = c.rhodecode_user.username
475
476 try:
477 user_group.group_data = new_state
478 Session().add(user_group)
479 Session().commit()
480
481 h.flash(_('User Group synchronization updated successfully'),
482 category='success')
483 except Exception:
484 log.exception("Exception during sync settings saving")
485 h.flash(_('An error occurred during synchronization update'),
486 category='error')
487
488 return redirect(
489 url('edit_user_group_advanced', user_group_id=user_group_id))
490
491 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
462 492 @XHRRequired()
463 493 @jsonify
464 494 def user_group_members(self, user_group_id):
@@ -50,7 +50,6 b' from rhodecode.model.user import UserMod'
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.permission import PermissionModel
52 52 from rhodecode.lib.utils import action_logger
53 from rhodecode.lib.ext_json import json
54 53 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
55 54
56 55 log = logging.getLogger(__name__)
@@ -76,51 +75,6 b' class UsersController(BaseController):'
76 75 ]
77 76 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
78 77
79 @HasPermissionAllDecorator('hg.admin')
80 def index(self):
81 """GET /users: All items in the collection"""
82 # url('users')
83
84 from rhodecode.lib.utils import PartialRenderer
85 _render = PartialRenderer('data_table/_dt_elements.mako')
86
87 def username(user_id, username):
88 return _render("user_name", user_id, username)
89
90 def user_actions(user_id, username):
91 return _render("user_actions", user_id, username)
92
93 # json generate
94 c.users_list = User.query()\
95 .filter(User.username != User.DEFAULT_USER) \
96 .all()
97
98 users_data = []
99 for user in c.users_list:
100 users_data.append({
101 "username": h.gravatar_with_user(user.username),
102 "username_raw": user.username,
103 "email": user.email,
104 "first_name": h.escape(user.name),
105 "last_name": h.escape(user.lastname),
106 "last_login": h.format_date(user.last_login),
107 "last_login_raw": datetime_to_time(user.last_login),
108 "last_activity": h.format_date(
109 h.time_to_datetime(user.user_data.get('last_activity', 0))),
110 "last_activity_raw": user.user_data.get('last_activity', 0),
111 "active": h.bool2icon(user.active),
112 "active_raw": user.active,
113 "admin": h.bool2icon(user.admin),
114 "admin_raw": user.admin,
115 "extern_type": user.extern_type,
116 "extern_name": user.extern_name,
117 "action": user_actions(user.user_id, user.username),
118 })
119
120
121 c.data = json.dumps(users_data)
122 return render('admin/users/users.mako')
123
124 78 def _get_personal_repo_group_template_vars(self):
125 79 DummyUser = AttributeDict({
126 80 'username': '${username}',
@@ -135,7 +89,6 b' class UsersController(BaseController):'
135 89 @auth.CSRFRequired()
136 90 def create(self):
137 91 """POST /users: Create a new item"""
138 # url('users')
139 92 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
140 93 user_model = UserModel()
141 94 user_form = UserForm()()
@@ -168,7 +121,7 b' class UsersController(BaseController):'
168 121 log.exception("Exception creation of user")
169 122 h.flash(_('Error occurred during creation of user %s')
170 123 % request.POST.get('username'), category='error')
171 return redirect(url('users'))
124 return redirect(h.route_path('users'))
172 125
173 126 @HasPermissionAllDecorator('hg.admin')
174 127 def new(self):
@@ -312,7 +265,7 b' class UsersController(BaseController):'
312 265 log.exception("Exception during deletion of user")
313 266 h.flash(_('An error occurred during deletion of user'),
314 267 category='error')
315 return redirect(url('users'))
268 return redirect(h.route_path('users'))
316 269
317 270 @HasPermissionAllDecorator('hg.admin')
318 271 @auth.CSRFRequired()
@@ -404,7 +357,7 b' class UsersController(BaseController):'
404 357 c.user = User.get_or_404(user_id)
405 358 if c.user.username == User.DEFAULT_USER:
406 359 h.flash(_("You can't edit this user"), category='warning')
407 return redirect(url('users'))
360 return redirect(h.route_path('users'))
408 361
409 362 c.active = 'profile'
410 363 c.extern_type = c.user.extern_type
@@ -425,7 +378,7 b' class UsersController(BaseController):'
425 378 user = c.user = User.get_or_404(user_id)
426 379 if user.username == User.DEFAULT_USER:
427 380 h.flash(_("You can't edit this user"), category='warning')
428 return redirect(url('users'))
381 return redirect(h.route_path('users'))
429 382
430 383 c.active = 'advanced'
431 384 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
@@ -452,83 +405,12 b' class UsersController(BaseController):'
452 405 force_defaults=False)
453 406
454 407 @HasPermissionAllDecorator('hg.admin')
455 def edit_auth_tokens(self, user_id):
456 user_id = safe_int(user_id)
457 c.user = User.get_or_404(user_id)
458 if c.user.username == User.DEFAULT_USER:
459 h.flash(_("You can't edit this user"), category='warning')
460 return redirect(url('users'))
461
462 c.active = 'auth_tokens'
463 show_expired = True
464 c.lifetime_values = [
465 (str(-1), _('forever')),
466 (str(5), _('5 minutes')),
467 (str(60), _('1 hour')),
468 (str(60 * 24), _('1 day')),
469 (str(60 * 24 * 30), _('1 month')),
470 ]
471 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
472 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
473 for x in AuthTokenModel.cls.ROLES]
474 c.role_options = [(c.role_values, _("Role"))]
475 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
476 c.user.user_id, show_expired=show_expired)
477 defaults = c.user.get_dict()
478 return htmlfill.render(
479 render('admin/users/user_edit.mako'),
480 defaults=defaults,
481 encoding="UTF-8",
482 force_defaults=False)
483
484 @HasPermissionAllDecorator('hg.admin')
485 @auth.CSRFRequired()
486 def add_auth_token(self, user_id):
487 user_id = safe_int(user_id)
488 c.user = User.get_or_404(user_id)
489 if c.user.username == User.DEFAULT_USER:
490 h.flash(_("You can't edit this user"), category='warning')
491 return redirect(url('users'))
492
493 lifetime = safe_int(request.POST.get('lifetime'), -1)
494 description = request.POST.get('description')
495 role = request.POST.get('role')
496 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
497 Session().commit()
498 h.flash(_("Auth token successfully created"), category='success')
499 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
500
501 @HasPermissionAllDecorator('hg.admin')
502 @auth.CSRFRequired()
503 def delete_auth_token(self, user_id):
504 user_id = safe_int(user_id)
505 c.user = User.get_or_404(user_id)
506 if c.user.username == User.DEFAULT_USER:
507 h.flash(_("You can't edit this user"), category='warning')
508 return redirect(url('users'))
509
510 auth_token = request.POST.get('del_auth_token')
511 if request.POST.get('del_auth_token_builtin'):
512 user = User.get(c.user.user_id)
513 if user:
514 user.api_key = generate_auth_token(user.username)
515 Session().add(user)
516 Session().commit()
517 h.flash(_("Auth token successfully reset"), category='success')
518 elif auth_token:
519 AuthTokenModel().delete(auth_token, c.user.user_id)
520 Session().commit()
521 h.flash(_("Auth token successfully deleted"), category='success')
522
523 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
524
525 @HasPermissionAllDecorator('hg.admin')
526 408 def edit_global_perms(self, user_id):
527 409 user_id = safe_int(user_id)
528 410 c.user = User.get_or_404(user_id)
529 411 if c.user.username == User.DEFAULT_USER:
530 412 h.flash(_("You can't edit this user"), category='warning')
531 return redirect(url('users'))
413 return redirect(h.route_path('users'))
532 414
533 415 c.active = 'global_perms'
534 416
@@ -602,7 +484,7 b' class UsersController(BaseController):'
602 484 c.user = User.get_or_404(user_id)
603 485 if c.user.username == User.DEFAULT_USER:
604 486 h.flash(_("You can't edit this user"), category='warning')
605 return redirect(url('users'))
487 return redirect(h.route_path('users'))
606 488
607 489 c.active = 'perms_summary'
608 490 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
@@ -615,7 +497,7 b' class UsersController(BaseController):'
615 497 c.user = User.get_or_404(user_id)
616 498 if c.user.username == User.DEFAULT_USER:
617 499 h.flash(_("You can't edit this user"), category='warning')
618 return redirect(url('users'))
500 return redirect(h.route_path('users'))
619 501
620 502 c.active = 'emails'
621 503 c.user_email_map = UserEmailMap.query() \
@@ -673,7 +555,7 b' class UsersController(BaseController):'
673 555 c.user = User.get_or_404(user_id)
674 556 if c.user.username == User.DEFAULT_USER:
675 557 h.flash(_("You can't edit this user"), category='warning')
676 return redirect(url('users'))
558 return redirect(h.route_path('users'))
677 559
678 560 c.active = 'ips'
679 561 c.user_ip_map = UserIpMap.query() \
@@ -28,12 +28,11 b' import pytz'
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 from beaker.cache import cache_region, region_invalidate
31 from beaker.cache import cache_region
32 32 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
33 33
34 from rhodecode.model.db import CacheKey
34 from rhodecode.model.db import CacheKey, UserApiKeys
35 35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import caches
37 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 37 from rhodecode.lib.base import BaseRepoController
39 38 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
@@ -62,7 +61,7 b' class FeedController(BaseRepoController)'
62 61 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
63 62 }
64 63
65 @LoginRequired(auth_token_access=True)
64 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
66 65 def __before__(self):
67 66 super(FeedController, self).__before__()
68 67 config = self._get_config()
@@ -223,6 +223,8 b' class FilesController(BaseRepoController'
223 223 c.file_author = True
224 224 c.file_tree = ''
225 225 if c.file.is_file():
226 c.lf_node = c.file.get_largefile_node()
227
226 228 c.file_source_page = 'true'
227 229 c.file_last_commit = c.file.last_commit
228 230 if c.file.size < self.cut_off_limit_file:
@@ -239,7 +241,11 b' class FilesController(BaseRepoController'
239 241
240 242 c.on_branch_head = self._is_valid_head(
241 243 commit_id, c.rhodecode_repo)
242 c.branch_or_raw_id = c.commit.branch or c.commit.raw_id
244
245 branch = c.commit.branch if (
246 c.commit.branch and '/' not in c.commit.branch) else None
247 c.branch_or_raw_id = branch or c.commit.raw_id
248 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
243 249
244 250 author = c.file_last_commit.author
245 251 c.authors = [(h.email(author),
@@ -260,6 +266,32 b' class FilesController(BaseRepoController'
260 266 return render('files/files.mako')
261 267
262 268 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
271 def annotate_previous(self, repo_name, revision, f_path):
272
273 commit_id = revision
274 commit = self.__get_commit_or_redirect(commit_id, repo_name)
275 prev_commit_id = commit.raw_id
276
277 f_path = f_path
278 is_file = False
279 try:
280 _file = commit.get_node(f_path)
281 is_file = _file.is_file()
282 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
283 pass
284
285 if is_file:
286 history = commit.get_file_history(f_path)
287 prev_commit_id = history[1].raw_id \
288 if len(history) > 1 else prev_commit_id
289
290 return redirect(h.url(
291 'files_annotate_home', repo_name=repo_name,
292 revision=prev_commit_id, f_path=f_path))
293
294 @LoginRequired()
263 295 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
264 296 'repository.admin')
265 297 @jsonify
@@ -317,6 +349,14 b' class FilesController(BaseRepoController'
317 349 commit = self.__get_commit_or_redirect(revision, repo_name)
318 350 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
319 351
352 if request.GET.get('lf'):
353 # only if lf get flag is passed, we download this file
354 # as LFS/Largefile
355 lf_node = file_node.get_largefile_node()
356 if lf_node:
357 # overwrite our pointer with the REAL large-file
358 file_node = lf_node
359
320 360 response.content_disposition = 'attachment; filename=%s' % \
321 361 safe_str(f_path.split(Repository.NAME_SEP)[-1])
322 362
@@ -352,6 +392,7 b' class FilesController(BaseRepoController'
352 392 'image/png': ('image/png', 'inline'),
353 393 'image/gif': ('image/gif', 'inline'),
354 394 'image/jpeg': ('image/jpeg', 'inline'),
395 'application/pdf': ('application/pdf', 'inline'),
355 396 }
356 397
357 398 mimetype = file_node.mimetype
@@ -34,8 +34,8 b' from webob.exc import HTTPBadRequest'
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 from rhodecode.controllers.admin.admin import _journal_filter
38 from rhodecode.model.db import UserLog, UserFollowing, User
37 from rhodecode.controllers.admin.admin import user_log_filter
38 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
39 39 from rhodecode.model.meta import Session
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.helpers import Page
@@ -89,7 +89,7 b' class JournalController(BaseController):'
89 89 .options(joinedload(UserLog.repository))
90 90 #filter
91 91 try:
92 journal = _journal_filter(journal, c.search_term)
92 journal = user_log_filter(journal, c.search_term)
93 93 except Exception:
94 94 # we want this to crash for now
95 95 raise
@@ -211,7 +211,7 b' class JournalController(BaseController):'
211 211
212 212 return render('journal/journal.mako')
213 213
214 @LoginRequired(auth_token_access=True)
214 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
215 215 @NotAnonymous()
216 216 def journal_atom(self):
217 217 """
@@ -223,7 +223,7 b' class JournalController(BaseController):'
223 223 .all()
224 224 return self._atom_feed(following, public=False)
225 225
226 @LoginRequired(auth_token_access=True)
226 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
227 227 @NotAnonymous()
228 228 def journal_rss(self):
229 229 """
@@ -281,7 +281,7 b' class JournalController(BaseController):'
281 281 return c.journal_data
282 282 return render('journal/public_journal.mako')
283 283
284 @LoginRequired(auth_token_access=True)
284 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
285 285 def public_journal_atom(self):
286 286 """
287 287 Produce an atom-1.0 feed via feedgenerator module
@@ -293,7 +293,7 b' class JournalController(BaseController):'
293 293
294 294 return self._atom_feed(c.following)
295 295
296 @LoginRequired(auth_token_access=True)
296 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
297 297 def public_journal_rss(self):
298 298 """
299 299 Produce an rss2 feed via feedgenerator module
@@ -69,6 +69,8 b' class PullrequestsController(BaseRepoCon'
69 69
70 70 def __before__(self):
71 71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
72 74
73 75 def _extract_ordering(self, request):
74 76 column_index = safe_int(request.GET.get('order[0][column]'))
@@ -440,13 +442,25 b' class PullrequestsController(BaseRepoCon'
440 442 resp = PullRequestModel().update_commits(pull_request)
441 443
442 444 if resp.executed:
445
446 if resp.target_changed and resp.source_changed:
447 changed = 'target and source repositories'
448 elif resp.target_changed and not resp.source_changed:
449 changed = 'target repository'
450 elif not resp.target_changed and resp.source_changed:
451 changed = 'source repository'
452 else:
453 changed = 'nothing'
454
443 455 msg = _(
444 456 u'Pull request updated to "{source_commit_id}" with '
445 u'{count_added} added, {count_removed} removed commits.')
457 u'{count_added} added, {count_removed} removed commits. '
458 u'Source of changes: {change_source}')
446 459 msg = msg.format(
447 460 source_commit_id=pull_request.source_ref_parts.commit_id,
448 461 count_added=len(resp.changes.added),
449 count_removed=len(resp.changes.removed))
462 count_removed=len(resp.changes.removed),
463 change_source=changed)
450 464 h.flash(msg, category='success')
451 465
452 466 registry = get_current_registry()
@@ -562,13 +576,21 b' class PullrequestsController(BaseRepoCon'
562 576 def delete(self, repo_name, pull_request_id):
563 577 pull_request_id = safe_int(pull_request_id)
564 578 pull_request = PullRequest.get_or_404(pull_request_id)
579
580 pr_closed = pull_request.is_closed()
581 allowed_to_delete = PullRequestModel().check_user_delete(
582 pull_request, c.rhodecode_user) and not pr_closed
583
565 584 # only owner can delete it !
566 if pull_request.author.user_id == c.rhodecode_user.user_id:
585 if allowed_to_delete:
567 586 PullRequestModel().delete(pull_request)
568 587 Session().commit()
569 588 h.flash(_('Successfully deleted pull request'),
570 589 category='success')
571 590 return redirect(url('my_account_pullrequests'))
591
592 h.flash(_('Your are not allowed to delete this pull request'),
593 category='error')
572 594 raise HTTPForbidden()
573 595
574 596 def _get_pr_version(self, pull_request_id, version=None):
@@ -642,6 +664,13 b' class PullrequestsController(BaseRepoCon'
642 664 pull_request_display_obj,
643 665 at_version) = self._get_pr_version(
644 666 pull_request_id, version=version)
667 pr_closed = pull_request_latest.is_closed()
668
669 if pr_closed and (version or from_version):
670 # not allow to browse versions
671 return redirect(h.url('pullrequest_show', repo_name=repo_name,
672 pull_request_id=pull_request_id))
673
645 674 versions = pull_request_display_obj.versions()
646 675
647 676 c.at_version = at_version
@@ -675,20 +704,21 b' class PullrequestsController(BaseRepoCon'
675 704 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
676 705 pull_request_at_ver)
677 706
678 c.ancestor = None # empty ancestor hidden in display
679 707 c.pull_request = pull_request_display_obj
680 708 c.pull_request_latest = pull_request_latest
681 709
682 pr_closed = pull_request_latest.is_closed()
683 710 if compare or (at_version and not at_version == 'latest'):
684 711 c.allowed_to_change_status = False
685 712 c.allowed_to_update = False
686 713 c.allowed_to_merge = False
687 714 c.allowed_to_delete = False
688 715 c.allowed_to_comment = False
716 c.allowed_to_close = False
689 717 else:
690 718 c.allowed_to_change_status = PullRequestModel(). \
691 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
719 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
720 and not pr_closed
721
692 722 c.allowed_to_update = PullRequestModel().check_user_update(
693 723 pull_request_latest, c.rhodecode_user) and not pr_closed
694 724 c.allowed_to_merge = PullRequestModel().check_user_merge(
@@ -696,6 +726,7 b' class PullrequestsController(BaseRepoCon'
696 726 c.allowed_to_delete = PullRequestModel().check_user_delete(
697 727 pull_request_latest, c.rhodecode_user) and not pr_closed
698 728 c.allowed_to_comment = not pr_closed
729 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
699 730
700 731 # check merge capabilities
701 732 _merge_check = MergeCheck.validate(
@@ -704,6 +735,7 b' class PullrequestsController(BaseRepoCon'
704 735 c.pr_merge_possible = not _merge_check.failed
705 736 c.pr_merge_message = _merge_check.merge_msg
706 737
738 c.pull_request_review_status = _merge_check.review_status
707 739 if merge_checks:
708 740 return render('/pullrequests/pullrequest_merge_checks.mako')
709 741
@@ -712,7 +744,6 b' class PullrequestsController(BaseRepoCon'
712 744 # reviewers and statuses
713 745 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
714 746 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
715 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
716 747
717 748 # GENERAL COMMENTS with versions #
718 749 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
@@ -789,12 +820,15 b' class PullrequestsController(BaseRepoCon'
789 820 target_commit = EmptyCommit()
790 821 c.missing_requirements = False
791 822
823 source_scm = source_repo.scm_instance()
824 target_scm = target_repo.scm_instance()
825
792 826 # try first shadow repo, fallback to regular repo
793 827 try:
794 828 commits_source_repo = pull_request_latest.get_shadow_repo()
795 829 except Exception:
796 830 log.debug('Failed to get shadow repo', exc_info=True)
797 commits_source_repo = source_repo.scm_instance()
831 commits_source_repo = source_scm
798 832
799 833 c.commits_source_repo = commits_source_repo
800 834 commit_cache = {}
@@ -818,6 +852,15 b' class PullrequestsController(BaseRepoCon'
818 852 'Failed to get all required data from repo', exc_info=True)
819 853 c.missing_requirements = True
820 854
855 c.ancestor = None # set it to None, to hide it from PR view
856
857 try:
858 ancestor_id = source_scm.get_common_ancestor(
859 source_commit.raw_id, target_commit.raw_id, target_scm)
860 c.ancestor_commit = source_scm.get_commit(ancestor_id)
861 except Exception:
862 c.ancestor_commit = None
863
821 864 c.statuses = source_repo.statuses(
822 865 [x.raw_id for x in c.commit_ranges])
823 866
@@ -860,12 +903,7 b' class PullrequestsController(BaseRepoCon'
860 903 # We need to swap that here to generate it properly on the html side
861 904 c.target_repo = c.source_repo
862 905
863 if c.allowed_to_update:
864 force_close = ('forced_closed', _('Close Pull Request'))
865 statuses = ChangesetStatus.STATUSES + [force_close]
866 else:
867 statuses = ChangesetStatus.STATUSES
868 c.commit_statuses = statuses
906 c.commit_statuses = ChangesetStatus.STATUSES
869 907
870 908 c.show_version_changes = not pr_closed
871 909 if c.show_version_changes:
@@ -924,22 +962,21 b' class PullrequestsController(BaseRepoCon'
924 962 if pull_request.is_closed():
925 963 raise HTTPForbidden()
926 964
927 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
928 # as a changeset status, still we want to send it in one value.
929 965 status = request.POST.get('changeset_status', None)
930 966 text = request.POST.get('text')
931 967 comment_type = request.POST.get('comment_type')
932 968 resolves_comment_id = request.POST.get('resolves_comment_id', None)
969 close_pull_request = request.POST.get('close_pull_request')
933 970
934 if status and '_closed' in status:
971 close_pr = False
972 if close_pull_request:
935 973 close_pr = True
936 status = status.replace('_closed', '')
937 else:
938 close_pr = False
939
940 forced = (status == 'forced')
941 if forced:
942 status = 'rejected'
974 pull_request_review_status = pull_request.calculated_review_status()
975 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
976 # approved only if we have voting consent
977 status = ChangesetStatus.STATUS_APPROVED
978 else:
979 status = ChangesetStatus.STATUS_REJECTED
943 980
944 981 allowed_to_change_status = PullRequestModel().check_user_change_status(
945 982 pull_request, c.rhodecode_user)
@@ -995,7 +1032,7 b' class PullrequestsController(BaseRepoCon'
995 1032 status_completed = (
996 1033 calculated_status in [ChangesetStatus.STATUS_APPROVED,
997 1034 ChangesetStatus.STATUS_REJECTED])
998 if forced or status_completed:
1035 if close_pull_request or status_completed:
999 1036 PullRequestModel().close_pull_request(
1000 1037 pull_request_id, c.rhodecode_user)
1001 1038 else:
@@ -38,7 +38,7 b' from rhodecode.lib.utils2 import safe_st'
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
40 40 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.lib.markup_renderer import MarkupRenderer
41 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
42 42 from rhodecode.lib.ext_json import json
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 44 from rhodecode.lib.vcs.exceptions import (
@@ -70,7 +70,12 b' class SummaryController(BaseRepoControll'
70 70 log.debug("Searching for a README file.")
71 71 readme_node = ReadmeFinder(default_renderer).search(commit)
72 72 if readme_node:
73 readme_data = self._render_readme_or_none(commit, readme_node)
73 relative_url = h.url('files_raw_home',
74 repo_name=repo_name,
75 revision=commit.raw_id,
76 f_path=readme_node.path)
77 readme_data = self._render_readme_or_none(
78 commit, readme_node, relative_url)
74 79 readme_filename = readme_node.path
75 80 return readme_data, readme_filename
76 81
@@ -95,13 +100,16 b' class SummaryController(BaseRepoControll'
95 100 log.exception(
96 101 "Problem getting commit when trying to render the README.")
97 102
98 def _render_readme_or_none(self, commit, readme_node):
103 def _render_readme_or_none(self, commit, readme_node, relative_url):
99 104 log.debug(
100 105 'Found README file `%s` rendering...', readme_node.path)
101 106 renderer = MarkupRenderer()
102 107 try:
103 return renderer.render(
108 html_source = renderer.render(
104 109 readme_node.content, filename=readme_node.path)
110 if relative_url:
111 return relative_links(html_source, relative_url)
112 return html_source
105 113 except Exception:
106 114 log.exception(
107 115 "Exception while trying to render the README")
@@ -56,8 +56,15 b' class RhodecodeEvent(object):'
56 56 @property
57 57 def actor(self):
58 58 auth_user = self.auth_user
59
59 60 if auth_user:
60 return auth_user.get_instance()
61 instance = auth_user.get_instance()
62 if not instance:
63 return AttributeDict(dict(
64 username=auth_user.username
65 ))
66 return instance
67
61 68 return SYSTEM_USER
62 69
63 70 @property
@@ -128,6 +128,9 b' class PullRequestCommentEvent(PullReques'
128 128 'comment': {
129 129 'status': status,
130 130 'text': self.comment.text,
131 'type': self.comment.comment_type,
132 'file': self.comment.f_path,
133 'line': self.comment.line_no,
131 134 'url': CommentsModel().get_url(self.comment)
132 135 }
133 136 })
@@ -34,10 +34,9 b' def _commits_as_dict(commit_ids, repos):'
34 34 :param repos: list of repos to check
35 35 """
36 36 from rhodecode.lib.utils2 import extract_mentioned_users
37 from rhodecode.model.db import Repository
38 37 from rhodecode.lib import helpers as h
39 from rhodecode.lib.helpers import process_patterns
40 from rhodecode.lib.helpers import urlify_commit_message
38 from rhodecode.lib.helpers import (
39 urlify_commit_message, process_patterns, chop_at_smart)
41 40
42 41 if not repos:
43 42 raise Exception('no repo defined')
@@ -78,14 +77,15 b' def _commits_as_dict(commit_ids, repos):'
78 77 cs_data['issues'] = issues_data
79 78 cs_data['message_html'] = urlify_commit_message(cs_data['message'],
80 79 repo.repo_name)
80 cs_data['message_html_title'] = chop_at_smart(cs_data['message'], '\n', suffix_if_chopped='...')
81 81 commits.append(cs_data)
82 82
83 83 needed_commits.remove(commit_id)
84 84
85 85 except Exception as e:
86 86 log.exception(e)
87 # we don't send any commits when crash happens, only full list matters
88 # we short circuit then.
87 # we don't send any commits when crash happens, only full list
88 # matters we short circuit then.
89 89 return []
90 90
91 91 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -19,12 +19,14 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from __future__ import unicode_literals
22 import re
23 import time
24 import textwrap
25 import logging
26
22 27 import deform
23 import re
24 import logging
25 28 import requests
26 29 import colander
27 import textwrap
28 30 from celery.task import task
29 31 from mako.template import Template
30 32
@@ -85,17 +87,6 b' class SlackSettingsSchema(colander.Schem'
85 87 )
86 88
87 89
88 repo_push_template = Template(r'''
89 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
90 %for branch, branch_commits in branches_commits.items():
91 branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
92 %for commit in branch_commits['commits']:
93 > <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
94 %endfor
95 %endfor
96 ''')
97
98
99 90 class SlackIntegrationType(IntegrationTypeBase):
100 91 key = 'slack'
101 92 display_name = _('Slack')
@@ -124,25 +115,31 b' class SlackIntegrationType(IntegrationTy'
124 115
125 116 data = event.as_dict()
126 117
118 # defaults
119 title = '*%s* caused a *%s* event' % (
120 data['actor']['username'], event.name)
127 121 text = '*%s* caused a *%s* event' % (
128 122 data['actor']['username'], event.name)
123 fields = None
124 overrides = None
129 125
130 126 log.debug('handling slack event for %s' % event.name)
131 127
132 128 if isinstance(event, events.PullRequestCommentEvent):
133 text = self.format_pull_request_comment_event(event, data)
129 (title, text, fields, overrides) \
130 = self.format_pull_request_comment_event(event, data)
134 131 elif isinstance(event, events.PullRequestReviewEvent):
135 text = self.format_pull_request_review_event(event, data)
132 title, text = self.format_pull_request_review_event(event, data)
136 133 elif isinstance(event, events.PullRequestEvent):
137 text = self.format_pull_request_event(event, data)
134 title, text = self.format_pull_request_event(event, data)
138 135 elif isinstance(event, events.RepoPushEvent):
139 text = self.format_repo_push_event(data)
136 title, text = self.format_repo_push_event(data)
140 137 elif isinstance(event, events.RepoCreateEvent):
141 text = self.format_repo_create_event(data)
138 title, text = self.format_repo_create_event(data)
142 139 else:
143 140 log.error('unhandled event type: %r' % event)
144 141
145 run_task(post_text_to_slack, self.settings, text)
142 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
146 143
147 144 def settings_schema(self):
148 145 schema = SlackSettingsSchema()
@@ -167,37 +164,60 b' class SlackIntegrationType(IntegrationTy'
167 164 comment_url=data['comment']['url'],
168 165 )
169 166
170 comment_status = ''
167 fields = None
168 overrides = None
169 status_text = None
170
171 171 if data['comment']['status']:
172 comment_status = '[{}]: '.format(data['comment']['status'])
172 status_color = {
173 'approved': '#0ac878',
174 'rejected': '#e85e4d'}.get(data['comment']['status'])
175
176 if status_color:
177 overrides = {"color": status_color}
178
179 status_text = data['comment']['status']
180
181 if data['comment']['file']:
182 fields = [
183 {
184 "title": "file",
185 "value": data['comment']['file']
186 },
187 {
188 "title": "line",
189 "value": data['comment']['line']
190 }
191 ]
173 192
174 return (textwrap.dedent(
175 '''
176 *{user}* commented on pull request <{pr_url}|#{number}> - {pr_title}:
177 >>> {comment_status}{comment_text}
178 ''').format(
179 comment_status=comment_status,
180 user=data['actor']['username'],
181 number=data['pullrequest']['pull_request_id'],
182 pr_url=data['pullrequest']['url'],
183 pr_status=data['pullrequest']['status'],
184 pr_title=data['pullrequest']['title'],
185 comment_text=comment_text
186 )
193 title = Template(textwrap.dedent(r'''
194 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
195 ''')).render(data=data, comment=event.comment)
196
197 text = Template(textwrap.dedent(r'''
198 *pull request title*: ${pr_title}
199 % if status_text:
200 *submitted status*: `${status_text}`
201 % endif
202 >>> ${comment_text}
203 ''')).render(comment_text=comment_text,
204 pr_title=data['pullrequest']['title'],
205 status_text=status_text)
206
207 return title, text, fields, overrides
208
209 def format_pull_request_review_event(self, event, data):
210 title = Template(textwrap.dedent(r'''
211 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
212 ''')).render(data=data)
213
214 text = Template(textwrap.dedent(r'''
215 *pull request title*: ${pr_title}
216 ''')).render(
217 pr_title=data['pullrequest']['title'],
187 218 )
188 219
189 def format_pull_request_review_event(self, event, data):
190 return (textwrap.dedent(
191 '''
192 Status changed to {pr_status} for pull request <{pr_url}|#{number}> - {pr_title}
193 ''').format(
194 user=data['actor']['username'],
195 number=data['pullrequest']['pull_request_id'],
196 pr_url=data['pullrequest']['url'],
197 pr_status=data['pullrequest']['status'],
198 pr_title=data['pullrequest']['title'],
199 )
200 )
220 return title, text
201 221
202 222 def format_pull_request_event(self, event, data):
203 223 action = {
@@ -207,15 +227,22 b' class SlackIntegrationType(IntegrationTy'
207 227 events.PullRequestCreateEvent: 'created',
208 228 }.get(event.__class__, str(event.__class__))
209 229
210 return ('Pull request <{url}|#{number}> - {title} '
211 '`{action}` by *{user}*').format(
212 user=data['actor']['username'],
213 number=data['pullrequest']['pull_request_id'],
214 url=data['pullrequest']['url'],
215 title=data['pullrequest']['title'],
216 action=action
230 title = Template(textwrap.dedent(r'''
231 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
232 ''')).render(data=data, action=action)
233
234 text = Template(textwrap.dedent(r'''
235 *pull request title*: ${pr_title}
236 %if data['pullrequest']['commits']:
237 *commits*: ${len(data['pullrequest']['commits'])}
238 %endif
239 ''')).render(
240 pr_title=data['pullrequest']['title'],
241 data=data
217 242 )
218 243
244 return title, text
245
219 246 def format_repo_push_event(self, data):
220 247 branch_data = {branch['name']: branch
221 248 for branch in data['push']['branches']}
@@ -230,20 +257,38 b' class SlackIntegrationType(IntegrationTy'
230 257 branch_commits = branches_commits[commit['branch']]
231 258 branch_commits['commits'].append(commit)
232 259
233 result = repo_push_template.render(
260 title = Template(r'''
261 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
262 ''').render(data=data)
263
264 repo_push_template = Template(textwrap.dedent(r'''
265 %for branch, branch_commits in branches_commits.items():
266 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
267 %for commit in branch_commits['commits']:
268 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html_title']|html_to_slack_links}
269 %endfor
270 %endfor
271 '''))
272
273 text = repo_push_template.render(
234 274 data=data,
235 275 branches_commits=branches_commits,
236 276 html_to_slack_links=html_to_slack_links,
237 277 )
238 return result
278
279 return title, text
239 280
240 281 def format_repo_create_event(self, data):
241 return '<{}|{}> ({}) repository created by *{}*'.format(
242 data['repo']['url'],
243 data['repo']['repo_name'],
244 data['repo']['repo_type'],
245 data['actor']['username'],
246 )
282 title = Template(r'''
283 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
284 ''').render(data=data)
285
286 text = Template(textwrap.dedent(r'''
287 repo_url: ${data['repo']['url']}
288 repo_type: ${data['repo']['repo_type']}
289 ''')).render(data=data)
290
291 return title, text
247 292
248 293
249 294 def html_to_slack_links(message):
@@ -252,12 +297,38 b' def html_to_slack_links(message):'
252 297
253 298
254 299 @task(ignore_result=True)
255 def post_text_to_slack(settings, text):
256 log.debug('sending %s to slack %s' % (text, settings['service']))
257 resp = requests.post(settings['service'], json={
300 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
301 log.debug('sending %s (%s) to slack %s' % (
302 title, text, settings['service']))
303
304 fields = fields or []
305 overrides = overrides or {}
306
307 message_data = {
308 "fallback": text,
309 "color": "#427cc9",
310 "pretext": title,
311 #"author_name": "Bobby Tables",
312 #"author_link": "http://flickr.com/bobby/",
313 #"author_icon": "http://flickr.com/icons/bobby.jpg",
314 #"title": "Slack API Documentation",
315 #"title_link": "https://api.slack.com/",
316 "text": text,
317 "fields": fields,
318 #"image_url": "http://my-website.com/path/to/image.jpg",
319 #"thumb_url": "http://example.com/path/to/thumb.png",
320 "footer": "RhodeCode",
321 #"footer_icon": "",
322 "ts": time.time(),
323 "mrkdwn_in": ["pretext", "text"]
324 }
325 message_data.update(overrides)
326 json_message = {
327 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
258 328 "channel": settings.get('channel', ''),
259 329 "username": settings.get('username', 'Rhodecode'),
260 "text": text,
261 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
262 })
330 "attachments": [message_data]
331 }
332
333 resp = requests.post(settings['service'], json=json_message)
263 334 resp.raise_for_status() # raise exception on a failed request
@@ -29,6 +29,7 b' from pyramid.httpexceptions import HTTPF'
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 from rhodecode.apps.admin.navigation import navigation_list
32 33 from rhodecode.lib import auth
33 34 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 35 from rhodecode.lib.utils2 import safe_int
@@ -36,7 +37,6 b' from rhodecode.lib.helpers import Page'
36 37 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 38 from rhodecode.model.scm import ScmModel
38 39 from rhodecode.model.integration import IntegrationModel
39 from rhodecode.admin.navigation import navigation_list
40 40 from rhodecode.translation import _
41 41 from rhodecode.integrations import integration_type_registry
42 42 from rhodecode.model.validation_schema.schemas.integration_schema import (
@@ -75,11 +75,10 b' class IntegrationSettingsViewBase(object'
75 75 repo_name = request.matchdict['repo_name']
76 76 self.repo = Repository.get_by_repo_name(repo_name)
77 77
78 if 'repo_group_name' in request.matchdict: # in group settings context
78 if 'repo_group_name' in request.matchdict: # in group settings context
79 79 repo_group_name = request.matchdict['repo_group_name']
80 80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
81 81
82
83 82 if 'integration' in request.matchdict: # integration type context
84 83 integration_type = request.matchdict['integration']
85 84 self.IntegrationType = integration_type_registry[integration_type]
@@ -380,12 +379,12 b' class GlobalIntegrationsView(Integration'
380 379
381 380 class RepoIntegrationsView(IntegrationSettingsViewBase):
382 381 def perm_check(self, user):
383 return auth.HasRepoPermissionAll('repository.admin'
384 )(repo_name=self.repo.repo_name, user=user)
382 return auth.HasRepoPermissionAll('repository.admin')(
383 repo_name=self.repo.repo_name, user=user)
385 384
386 385
387 386 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
388 387 def perm_check(self, user):
389 return auth.HasRepoGroupPermissionAll('group.admin'
390 )(group_name=self.repo_group.group_name, user=user)
388 return auth.HasRepoGroupPermissionAll('group.admin')(
389 group_name=self.repo_group.group_name, user=user)
391 390
@@ -22,24 +22,22 b''
22 22 authentication and permission libraries
23 23 """
24 24
25 import os
25 26 import inspect
26 27 import collections
27 28 import fnmatch
28 29 import hashlib
29 30 import itertools
30 31 import logging
31 import os
32 32 import random
33 import time
34 33 import traceback
35 34 from functools import wraps
36 35
37 36 import ipaddress
38 from pyramid.httpexceptions import HTTPForbidden
37 from pyramid.httpexceptions import HTTPForbidden, HTTPFound
39 38 from pylons import url, request
40 39 from pylons.controllers.util import abort, redirect
41 40 from pylons.i18n.translation import _
42 from sqlalchemy import or_
43 41 from sqlalchemy.orm.exc import ObjectDeletedError
44 42 from sqlalchemy.orm import joinedload
45 43 from zope.cachedescriptors.property import Lazy as LazyProperty
@@ -99,6 +97,7 b' class PasswordGenerator(object):'
99 97
100 98
101 99 class _RhodeCodeCryptoBase(object):
100 ENC_PREF = None
102 101
103 102 def hash_create(self, str_):
104 103 """
@@ -139,6 +138,7 b' class _RhodeCodeCryptoBase(object):'
139 138
140 139
141 140 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
141 ENC_PREF = '$2a$10'
142 142
143 143 def hash_create(self, str_):
144 144 self._assert_bytes(str_)
@@ -194,6 +194,7 b' class _RhodeCodeCryptoBCrypt(_RhodeCodeC'
194 194
195 195
196 196 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
197 ENC_PREF = '_'
197 198
198 199 def hash_create(self, str_):
199 200 self._assert_bytes(str_)
@@ -211,6 +212,7 b' class _RhodeCodeCryptoSha256(_RhodeCodeC'
211 212
212 213
213 214 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
215 ENC_PREF = '_'
214 216
215 217 def hash_create(self, str_):
216 218 self._assert_bytes(str_)
@@ -567,8 +569,14 b' class PermissionCalculator(object):'
567 569 # on given user group
568 570 for perm in self.default_user_group_perms:
569 571 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
570 p = perm.Permission.permission_name
571 572 o = PermOrigin.USERGROUP_DEFAULT
573 if perm.UserGroup.user_id == self.user_id:
574 # set admin if owner
575 p = 'usergroup.admin'
576 o = PermOrigin.USERGROUP_OWNER
577 else:
578 p = perm.Permission.permission_name
579
572 580 # if we decide this user isn't inheriting permissions from default
573 581 # user we set him to .none so only explicit permissions work
574 582 if not user_inherit_object_permissions:
@@ -647,7 +655,7 b' class PermissionCalculator(object):'
647 655 multiple_counter[g_k] += 1
648 656 p = perm.Permission.permission_name
649 657 if perm.RepoGroup.user_id == self.user_id:
650 # set admin if owner
658 # set admin if owner, even for member of other user group
651 659 p = 'group.admin'
652 660 o = PermOrigin.REPOGROUP_OWNER
653 661 else:
@@ -683,7 +691,7 b' class PermissionCalculator(object):'
683 691 # user group for user group permissions
684 692 user_group_from_user_group = Permission\
685 693 .get_default_user_group_perms_from_user_group(
686 self.user_id, self.scope_repo_group_id)
694 self.user_id, self.scope_user_group_id)
687 695
688 696 multiple_counter = collections.defaultdict(int)
689 697 for perm in user_group_from_user_group:
@@ -694,9 +702,15 b' class PermissionCalculator(object):'
694 702 o = PermOrigin.USERGROUP_USERGROUP % u_k
695 703 multiple_counter[g_k] += 1
696 704 p = perm.Permission.permission_name
697 if multiple_counter[g_k] > 1:
698 cur_perm = self.permissions_user_groups[g_k]
699 p = self._choose_permission(p, cur_perm)
705
706 if perm.UserGroup.user_id == self.user_id:
707 # set admin if owner, even for member of other user group
708 p = 'usergroup.admin'
709 o = PermOrigin.USERGROUP_OWNER
710 else:
711 if multiple_counter[g_k] > 1:
712 cur_perm = self.permissions_user_groups[g_k]
713 p = self._choose_permission(p, cur_perm)
700 714 self.permissions_user_groups[g_k] = p, o
701 715
702 716 # user explicit permission for user groups
@@ -705,12 +719,18 b' class PermissionCalculator(object):'
705 719 for perm in user_user_groups_perms:
706 720 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
707 721 u_k = perm.UserUserGroupToPerm.user.username
708 p = perm.Permission.permission_name
709 722 o = PermOrigin.USERGROUP_USER % u_k
710 if not self.explicit:
711 cur_perm = self.permissions_user_groups.get(
712 ug_k, 'usergroup.none')
713 p = self._choose_permission(p, cur_perm)
723
724 if perm.UserGroup.user_id == self.user_id:
725 # set admin if owner
726 p = 'usergroup.admin'
727 o = PermOrigin.USERGROUP_OWNER
728 else:
729 p = perm.Permission.permission_name
730 if not self.explicit:
731 cur_perm = self.permissions_user_groups.get(
732 ug_k, 'usergroup.none')
733 p = self._choose_permission(p, cur_perm)
714 734 self.permissions_user_groups[ug_k] = p, o
715 735
716 736 def _choose_permission(self, new_perm, cur_perm):
@@ -831,10 +851,6 b' class AuthUser(object):'
831 851 self._permissions_scoped_cache[cache_key] = res
832 852 return self._permissions_scoped_cache[cache_key]
833 853
834 @property
835 def auth_tokens(self):
836 return self.get_auth_tokens()
837
838 854 def get_instance(self):
839 855 return User.get(self.user_id)
840 856
@@ -925,16 +941,6 b' class AuthUser(object):'
925 941 log.debug('PERMISSION tree computed %s' % (result_repr,))
926 942 return result
927 943
928 def get_auth_tokens(self):
929 auth_tokens = [self.api_key]
930 for api_key in UserApiKeys.query()\
931 .filter(UserApiKeys.user_id == self.user_id)\
932 .filter(or_(UserApiKeys.expires == -1,
933 UserApiKeys.expires >= time.time())).all():
934 auth_tokens.append(api_key.api_key)
935
936 return auth_tokens
937
938 944 @property
939 945 def is_default(self):
940 946 return self.username == User.DEFAULT_USER
@@ -952,25 +958,27 b' class AuthUser(object):'
952 958 """
953 959 Returns list of repositories you're an admin of
954 960 """
955 return [x[0] for x in self.permissions['repositories'].iteritems()
956 if x[1] == 'repository.admin']
961 return [
962 x[0] for x in self.permissions['repositories'].iteritems()
963 if x[1] == 'repository.admin']
957 964
958 965 @property
959 966 def repository_groups_admin(self):
960 967 """
961 968 Returns list of repository groups you're an admin of
962 969 """
963 return [x[0]
964 for x in self.permissions['repositories_groups'].iteritems()
965 if x[1] == 'group.admin']
970 return [
971 x[0] for x in self.permissions['repositories_groups'].iteritems()
972 if x[1] == 'group.admin']
966 973
967 974 @property
968 975 def user_groups_admin(self):
969 976 """
970 977 Returns list of user groups you're an admin of
971 978 """
972 return [x[0] for x in self.permissions['user_groups'].iteritems()
973 if x[1] == 'usergroup.admin']
979 return [
980 x[0] for x in self.permissions['user_groups'].iteritems()
981 if x[1] == 'usergroup.admin']
974 982
975 983 @property
976 984 def ip_allowed(self):
@@ -1171,7 +1179,7 b' class LoginRequired(object):'
1171 1179 :param api_access: if enabled this checks only for valid auth token
1172 1180 and grants access based on valid token
1173 1181 """
1174 def __init__(self, auth_token_access=False):
1182 def __init__(self, auth_token_access=None):
1175 1183 self.auth_token_access = auth_token_access
1176 1184
1177 1185 def __call__(self, func):
@@ -1191,7 +1199,7 b' class LoginRequired(object):'
1191 1199 ip_access_valid = False
1192 1200
1193 1201 # check if we used an APIKEY and it's a valid one
1194 # defined whitelist of controllers which API access will be enabled
1202 # defined white-list of controllers which API access will be enabled
1195 1203 _auth_token = request.GET.get(
1196 1204 'auth_token', '') or request.GET.get('api_key', '')
1197 1205 auth_token_access_valid = allowed_auth_token_access(
@@ -1200,8 +1208,20 b' class LoginRequired(object):'
1200 1208 # explicit controller is enabled or API is in our whitelist
1201 1209 if self.auth_token_access or auth_token_access_valid:
1202 1210 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1211 db_user = user.get_instance()
1203 1212
1204 if _auth_token and _auth_token in user.auth_tokens:
1213 if db_user:
1214 if self.auth_token_access:
1215 roles = self.auth_token_access
1216 else:
1217 roles = [UserApiKeys.ROLE_HTTP]
1218 token_match = db_user.authenticate_by_token(
1219 _auth_token, roles=roles)
1220 else:
1221 log.debug('Unable to fetch db instance for auth user: %s', user)
1222 token_match = False
1223
1224 if _auth_token and token_match:
1205 1225 auth_token_access_valid = True
1206 1226 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1207 1227 else:
@@ -1234,7 +1254,6 b' class LoginRequired(object):'
1234 1254 auth_token_access_valid))
1235 1255 # we preserve the get PARAM
1236 1256 came_from = request.path_qs
1237
1238 1257 log.debug('redirecting to login page with %s' % (came_from,))
1239 1258 return redirect(
1240 1259 h.route_path('login', _query={'came_from': came_from}))
@@ -1249,6 +1268,7 b' class NotAnonymous(object):'
1249 1268 return get_cython_compat_decorator(self.__wrapper, func)
1250 1269
1251 1270 def __wrapper(self, func, *fargs, **fkwargs):
1271 import rhodecode.lib.helpers as h
1252 1272 cls = fargs[0]
1253 1273 self.user = cls._rhodecode_user
1254 1274
@@ -1258,8 +1278,6 b' class NotAnonymous(object):'
1258 1278
1259 1279 if anonymous:
1260 1280 came_from = request.path_qs
1261
1262 import rhodecode.lib.helpers as h
1263 1281 h.flash(_('You need to be a registered user to '
1264 1282 'perform this action'),
1265 1283 category='warning')
@@ -1296,6 +1314,7 b' class HasAcceptedRepoType(object):'
1296 1314 return get_cython_compat_decorator(self.__wrapper, func)
1297 1315
1298 1316 def __wrapper(self, func, *fargs, **fkwargs):
1317 import rhodecode.lib.helpers as h
1299 1318 cls = fargs[0]
1300 1319 rhodecode_repo = cls.rhodecode_repo
1301 1320
@@ -1306,7 +1325,6 b' class HasAcceptedRepoType(object):'
1306 1325 if rhodecode_repo.alias in self.repo_type_list:
1307 1326 return func(*fargs, **fkwargs)
1308 1327 else:
1309 import rhodecode.lib.helpers as h
1310 1328 h.flash(h.literal(
1311 1329 _('Action not supported for %s.' % rhodecode_repo.alias)),
1312 1330 category='warning')
@@ -1326,7 +1344,22 b' class PermsDecorator(object):'
1326 1344 def __call__(self, func):
1327 1345 return get_cython_compat_decorator(self.__wrapper, func)
1328 1346
1347 def _get_request(self):
1348 from pyramid.threadlocal import get_current_request
1349 pyramid_request = get_current_request()
1350 if not pyramid_request:
1351 # return global request of pylons in case pyramid isn't available
1352 return request
1353 return pyramid_request
1354
1355 def _get_came_from(self):
1356 _request = self._get_request()
1357
1358 # both pylons/pyramid has this attribute
1359 return _request.path_qs
1360
1329 1361 def __wrapper(self, func, *fargs, **fkwargs):
1362 import rhodecode.lib.helpers as h
1330 1363 cls = fargs[0]
1331 1364 _user = cls._rhodecode_user
1332 1365
@@ -1342,17 +1375,15 b' class PermsDecorator(object):'
1342 1375 anonymous = _user.username == User.DEFAULT_USER
1343 1376
1344 1377 if anonymous:
1345 came_from = request.path_qs
1346
1347 import rhodecode.lib.helpers as h
1378 came_from = self._get_came_from()
1348 1379 h.flash(_('You need to be signed in to view this page'),
1349 category='warning')
1350 return redirect(
1380 category='warning')
1381 raise HTTPFound(
1351 1382 h.route_path('login', _query={'came_from': came_from}))
1352 1383
1353 1384 else:
1354 1385 # redirect with forbidden ret code
1355 return abort(403)
1386 raise HTTPForbidden()
1356 1387
1357 1388 def check_permissions(self, user):
1358 1389 """Dummy function for overriding"""
@@ -1391,10 +1422,13 b' class HasRepoPermissionAllDecorator(Perm'
1391 1422 Checks for access permission for all given predicates for specific
1392 1423 repository. All of them have to be meet in order to fulfill the request
1393 1424 """
1425 def _get_repo_name(self):
1426 _request = self._get_request()
1427 return get_repo_slug(_request)
1394 1428
1395 1429 def check_permissions(self, user):
1396 1430 perms = user.permissions
1397 repo_name = get_repo_slug(request)
1431 repo_name = self._get_repo_name()
1398 1432 try:
1399 1433 user_perms = set([perms['repositories'][repo_name]])
1400 1434 except KeyError:
@@ -1409,10 +1443,13 b' class HasRepoPermissionAnyDecorator(Perm'
1409 1443 Checks for access permission for any of given predicates for specific
1410 1444 repository. In order to fulfill the request any of predicates must be meet
1411 1445 """
1446 def _get_repo_name(self):
1447 _request = self._get_request()
1448 return get_repo_slug(_request)
1412 1449
1413 1450 def check_permissions(self, user):
1414 1451 perms = user.permissions
1415 repo_name = get_repo_slug(request)
1452 repo_name = self._get_repo_name()
1416 1453 try:
1417 1454 user_perms = set([perms['repositories'][repo_name]])
1418 1455 except KeyError:
@@ -1429,10 +1466,13 b' class HasRepoGroupPermissionAllDecorator'
1429 1466 repository group. All of them have to be meet in order to
1430 1467 fulfill the request
1431 1468 """
1469 def _get_repo_group_name(self):
1470 _request = self._get_request()
1471 return get_repo_group_slug(_request)
1432 1472
1433 1473 def check_permissions(self, user):
1434 1474 perms = user.permissions
1435 group_name = get_repo_group_slug(request)
1475 group_name = self._get_repo_group_name()
1436 1476 try:
1437 1477 user_perms = set([perms['repositories_groups'][group_name]])
1438 1478 except KeyError:
@@ -1449,10 +1489,13 b' class HasRepoGroupPermissionAnyDecorator'
1449 1489 repository group. In order to fulfill the request any
1450 1490 of predicates must be met
1451 1491 """
1492 def _get_repo_group_name(self):
1493 _request = self._get_request()
1494 return get_repo_group_slug(_request)
1452 1495
1453 1496 def check_permissions(self, user):
1454 1497 perms = user.permissions
1455 group_name = get_repo_group_slug(request)
1498 group_name = self._get_repo_group_name()
1456 1499 try:
1457 1500 user_perms = set([perms['repositories_groups'][group_name]])
1458 1501 except KeyError:
@@ -1468,10 +1511,13 b' class HasUserGroupPermissionAllDecorator'
1468 1511 Checks for access permission for all given predicates for specific
1469 1512 user group. All of them have to be meet in order to fulfill the request
1470 1513 """
1514 def _get_user_group_name(self):
1515 _request = self._get_request()
1516 return get_user_group_slug(_request)
1471 1517
1472 1518 def check_permissions(self, user):
1473 1519 perms = user.permissions
1474 group_name = get_user_group_slug(request)
1520 group_name = self._get_user_group_name()
1475 1521 try:
1476 1522 user_perms = set([perms['user_groups'][group_name]])
1477 1523 except KeyError:
@@ -1487,10 +1533,13 b' class HasUserGroupPermissionAnyDecorator'
1487 1533 Checks for access permission for any of given predicates for specific
1488 1534 user group. In order to fulfill the request any of predicates must be meet
1489 1535 """
1536 def _get_user_group_name(self):
1537 _request = self._get_request()
1538 return get_user_group_slug(_request)
1490 1539
1491 1540 def check_permissions(self, user):
1492 1541 perms = user.permissions
1493 group_name = get_user_group_slug(request)
1542 group_name = self._get_user_group_name()
1494 1543 try:
1495 1544 user_perms = set([perms['user_groups'][group_name]])
1496 1545 except KeyError:
@@ -1553,6 +1602,14 b' class PermsFunction(object):'
1553 1602 check_scope, user, check_location)
1554 1603 return False
1555 1604
1605 def _get_request(self):
1606 from pyramid.threadlocal import get_current_request
1607 pyramid_request = get_current_request()
1608 if not pyramid_request:
1609 # return global request of pylons incase pyramid one isn't available
1610 return request
1611 return pyramid_request
1612
1556 1613 def _get_check_scope(self, cls_name):
1557 1614 return {
1558 1615 'HasPermissionAll': 'GLOBAL',
@@ -1591,10 +1648,14 b' class HasRepoPermissionAll(PermsFunction'
1591 1648 self.repo_name = repo_name
1592 1649 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1593 1650
1594 def check_permissions(self, user):
1651 def _get_repo_name(self):
1595 1652 if not self.repo_name:
1596 self.repo_name = get_repo_slug(request)
1653 _request = self._get_request()
1654 self.repo_name = get_repo_slug(_request)
1655 return self.repo_name
1597 1656
1657 def check_permissions(self, user):
1658 self.repo_name = self._get_repo_name()
1598 1659 perms = user.permissions
1599 1660 try:
1600 1661 user_perms = set([perms['repositories'][self.repo_name]])
@@ -1610,10 +1671,13 b' class HasRepoPermissionAny(PermsFunction'
1610 1671 self.repo_name = repo_name
1611 1672 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1612 1673
1613 def check_permissions(self, user):
1674 def _get_repo_name(self):
1614 1675 if not self.repo_name:
1615 1676 self.repo_name = get_repo_slug(request)
1677 return self.repo_name
1616 1678
1679 def check_permissions(self, user):
1680 self.repo_name = self._get_repo_name()
1617 1681 perms = user.permissions
1618 1682 try:
1619 1683 user_perms = set([perms['repositories'][self.repo_name]])
@@ -1905,3 +1969,5 b' def get_cython_compat_decorator(wrapper,'
1905 1969 return wrapper(func, *args, **kwds)
1906 1970 local_wrapper.__wrapped__ = func
1907 1971 return local_wrapper
1972
1973
@@ -209,11 +209,12 b' def vcs_operation_context('
209 209 class BasicAuth(AuthBasicAuthenticator):
210 210
211 211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 initial_call_detection=False):
212 initial_call_detection=False, acl_repo_name=None):
213 213 self.realm = realm
214 214 self.initial_call = initial_call_detection
215 215 self.authfunc = authfunc
216 216 self.registry = registry
217 self.acl_repo_name = acl_repo_name
217 218 self._rc_auth_http_code = auth_http_code
218 219
219 220 def _get_response_from_code(self, http_code):
@@ -247,7 +248,7 b' class BasicAuth(AuthBasicAuthenticator):'
247 248 username, password = _parts
248 249 if self.authfunc(
249 250 username, password, environ, VCS_TYPE,
250 registry=self.registry):
251 registry=self.registry, acl_repo_name=self.acl_repo_name):
251 252 return username
252 253 if username and password:
253 254 # we mark that we actually executed authentication once, at
@@ -488,21 +489,12 b' class BaseController(WSGIController):'
488 489 _route_name)
489 490 )
490 491
491 # TODO: Maybe this should be move to pyramid to cover all views.
492 # check user attributes for password change flag
493 492 user_obj = auth_user.get_instance()
494 493 if user_obj and user_obj.user_data.get('force_password_change'):
495 494 h.flash('You are required to change your password', 'warning',
496 495 ignore_duplicate=True)
497
498 skip_user_check_urls = [
499 'error.document', 'login.logout', 'login.index',
500 'admin/my_account.my_account_password',
501 'admin/my_account.my_account_password_update'
502 ]
503 if _route_name not in skip_user_check_urls:
504 return self._dispatch_redirect(
505 url('my_account_password'), environ, start_response)
496 return self._dispatch_redirect(
497 url('my_account_password'), environ, start_response)
506 498
507 499 return WSGIController.__call__(self, environ, start_response)
508 500
@@ -226,7 +226,6 b' def vcsconnection(func):'
226 226 for alias in rhodecode.BACKENDS.keys():
227 227 if alias not in backends:
228 228 del rhodecode.BACKENDS[alias]
229 utils.configure_pyro4(settings)
230 229 utils.configure_vcs(settings)
231 230 connect_vcs(
232 231 settings['vcs.server'],
@@ -25,7 +25,7 b' from itertools import groupby'
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from rhodecode.lib.helpers import (
28 get_lexer_for_filenode, html_escape)
28 get_lexer_for_filenode, html_escape, get_custom_lexer)
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.lib.vcs.nodes import FileNode
31 31 from rhodecode.lib.diff_match_patch import diff_match_patch
@@ -407,8 +407,12 b' class DiffSet(object):'
407 407 if filename not in self._lexer_cache:
408 408 if filenode:
409 409 lexer = filenode.lexer
410 extension = filenode.extension
410 411 else:
411 412 lexer = FileNode.get_lexer(filename=filename)
413 extension = filename.split('.')[-1]
414
415 lexer = get_custom_lexer(extension) or lexer
412 416 self._lexer_cache[filename] = lexer
413 417 return self._lexer_cache[filename]
414 418
@@ -292,13 +292,13 b' class DbManage(object):'
292 292 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
293 293
294 294 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
295 TEST_USER_ADMIN_EMAIL, True)
295 TEST_USER_ADMIN_EMAIL, True, api_key=True)
296 296
297 297 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
298 TEST_USER_REGULAR_EMAIL, False)
298 TEST_USER_REGULAR_EMAIL, False, api_key=True)
299 299
300 300 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
301 TEST_USER_REGULAR2_EMAIL, False)
301 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
302 302
303 303 def create_ui_settings(self, repo_store_path):
304 304 """
@@ -306,6 +306,8 b' class DbManage(object):'
306 306 and disables dotencode
307 307 """
308 308 settings_model = SettingsModel(sa=self.sa)
309 from rhodecode.lib.vcs.backends.hg import largefiles_store
310 from rhodecode.lib.vcs.backends.git import lfs_store
309 311
310 312 # Build HOOKS
311 313 hooks = [
@@ -315,6 +317,7 b' class DbManage(object):'
315 317 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
316 318 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
317 319 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
320 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
318 321 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
319 322
320 323 ]
@@ -335,14 +338,23 b' class DbManage(object):'
335 338 self.sa.add(largefiles)
336 339
337 340 # set default largefiles cache dir, defaults to
338 # /repo location/.cache/largefiles
341 # /repo_store_location/.cache/largefiles
339 342 largefiles = RhodeCodeUi()
340 343 largefiles.ui_section = 'largefiles'
341 344 largefiles.ui_key = 'usercache'
342 largefiles.ui_value = os.path.join(repo_store_path, '.cache',
343 'largefiles')
345 largefiles.ui_value = largefiles_store(repo_store_path)
346
344 347 self.sa.add(largefiles)
345 348
349 # set default lfs cache dir, defaults to
350 # /repo_store_location/.cache/lfs_store
351 lfsstore = RhodeCodeUi()
352 lfsstore.ui_section = 'vcs_git_lfs'
353 lfsstore.ui_key = 'store_location'
354 lfsstore.ui_value = lfs_store(repo_store_path)
355
356 self.sa.add(lfsstore)
357
346 358 # enable hgsubversion disabled by default
347 359 hgsubversion = RhodeCodeUi()
348 360 hgsubversion.ui_section = 'extensions'
@@ -506,12 +518,12 b' class DbManage(object):'
506 518 self.create_ui_settings(path)
507 519
508 520 ui_config = [
509 ('web', 'push_ssl', 'false'),
521 ('web', 'push_ssl', 'False'),
510 522 ('web', 'allow_archive', 'gz zip bz2'),
511 523 ('web', 'allow_push', '*'),
512 524 ('web', 'baseurl', '/'),
513 525 ('paths', '/', path),
514 ('phases', 'publish', 'true')
526 ('phases', 'publish', 'True')
515 527 ]
516 528 for section, key, value in ui_config:
517 529 ui_conf = RhodeCodeUi()
@@ -560,7 +572,9 b' class DbManage(object):'
560 572
561 573 if api_key:
562 574 log.info('setting a provided api key for the user %s', username)
563 user.api_key = api_key
575 from rhodecode.model.auth_token import AuthTokenModel
576 AuthTokenModel().create(
577 user=user, description='BUILTIN TOKEN')
564 578
565 579 def create_default_user(self):
566 580 log.info('creating default user')
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
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
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
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
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: file copied from rhodecode/templates/admin/user_groups/user_groups.mako to rhodecode/templates/admin/users/user_edit_groups.mako
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
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
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
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
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