Show More
@@ -0,0 +1,55 b'' | |||
|
1 | |RCE| 4.1.0 |RNS| | |
|
2 | ----------------- | |
|
3 | ||
|
4 | Release Date | |
|
5 | ^^^^^^^^^^^^ | |
|
6 | ||
|
7 | - 2016-06-XX | |
|
8 | ||
|
9 | General | |
|
10 | ^^^^^^^ | |
|
11 | ||
|
12 | - Migrated more views to Pyramid. Those now include login, social plugins, search | |
|
13 | - Started Implementing Pyramid Events system in exchange to rcextensions callbacks | |
|
14 | - JS routes assets are now generated in development mode automatically | |
|
15 | - ini: Add fallback authentication plugin setting. In case only one | |
|
16 | authentication backend is enabled users can now enable fallback auth if | |
|
17 | they cannot log-in due to external servers being down | |
|
18 | - Bumped Mercurial to 3.8.3 version | |
|
19 | - Bumped RhodeCode Tools to 0.8.3 version | |
|
20 | ||
|
21 | New Features | |
|
22 | ^^^^^^^^^^^^ | |
|
23 | ||
|
24 | - search: add syntax highlighting, line numbers and line context to file | |
|
25 | content search results | |
|
26 | - Go To switcher now searches commit hashes as well | |
|
27 | - Token based authentication is now in CE edition as well | |
|
28 | - User groups: added autocomplete widget to be able to select members of | |
|
29 | other group as part of current group. | |
|
30 | ||
|
31 | Security | |
|
32 | ^^^^^^^^ | |
|
33 | ||
|
34 | - Added new action loggers for actions like adding/revoking permissions. | |
|
35 | - permissions: show origin of permissions in permissions summary. Allows users | |
|
36 | to see where and how permissions are inherited | |
|
37 | ||
|
38 | Performance | |
|
39 | ^^^^^^^^^^^ | |
|
40 | ||
|
41 | ||
|
42 | ||
|
43 | Fixes | |
|
44 | ^^^^^ | |
|
45 | ||
|
46 | - api: gracefully handle errors on repos that are damaged or missing | |
|
47 | from filesystem. | |
|
48 | - logging: log the original error when a merge failure occurs | |
|
49 | - #3965 Cannot change the owner of a user's group by using the API | |
|
50 | - database is now initialized inside pyramid context | |
|
51 | - fixed wrong check on LDAP plugin about missing ldap server | |
|
52 | - Bring back multi-threaded workers to gunicorn for backward compatibility with | |
|
53 | previous RhodeCode versions | |
|
54 | - Commit dates are now properly handled as UTC. This fixes some issues | |
|
55 | with displaying age of commits No newline at end of file |
@@ -0,0 +1,225 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2012-2016 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 colander | |
|
22 | import logging | |
|
23 | ||
|
24 | from sqlalchemy.ext.hybrid import hybrid_property | |
|
25 | ||
|
26 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin | |
|
27 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
|
28 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
|
29 | from rhodecode.lib.colander_utils import strip_whitespace | |
|
30 | from rhodecode.lib.utils2 import str2bool, safe_unicode | |
|
31 | from rhodecode.model.db import User | |
|
32 | from rhodecode.translation import _ | |
|
33 | ||
|
34 | ||
|
35 | log = logging.getLogger(__name__) | |
|
36 | ||
|
37 | ||
|
38 | def plugin_factory(plugin_id, *args, **kwds): | |
|
39 | """ | |
|
40 | Factory function that is called during plugin discovery. | |
|
41 | It returns the plugin instance. | |
|
42 | """ | |
|
43 | plugin = RhodeCodeAuthPlugin(plugin_id) | |
|
44 | return plugin | |
|
45 | ||
|
46 | ||
|
47 | class HeadersAuthnResource(AuthnPluginResourceBase): | |
|
48 | pass | |
|
49 | ||
|
50 | ||
|
51 | class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase): | |
|
52 | header = colander.SchemaNode( | |
|
53 | colander.String(), | |
|
54 | default='REMOTE_USER', | |
|
55 | description=_('Header to extract the user from'), | |
|
56 | preparer=strip_whitespace, | |
|
57 | title=_('Header'), | |
|
58 | widget='string') | |
|
59 | fallback_header = colander.SchemaNode( | |
|
60 | colander.String(), | |
|
61 | default='HTTP_X_FORWARDED_USER', | |
|
62 | description=_('Header to extract the user from when main one fails'), | |
|
63 | preparer=strip_whitespace, | |
|
64 | title=_('Fallback header'), | |
|
65 | widget='string') | |
|
66 | clean_username = colander.SchemaNode( | |
|
67 | colander.Boolean(), | |
|
68 | default=True, | |
|
69 | description=_('Perform cleaning of user, if passed user has @ in ' | |
|
70 | 'username then first part before @ is taken. ' | |
|
71 | 'If there\'s \\ in the username only the part after ' | |
|
72 | ' \\ is taken'), | |
|
73 | missing=False, | |
|
74 | title=_('Clean username'), | |
|
75 | widget='bool') | |
|
76 | ||
|
77 | ||
|
78 | class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin): | |
|
79 | ||
|
80 | def includeme(self, config): | |
|
81 | config.add_authn_plugin(self) | |
|
82 | config.add_authn_resource(self.get_id(), HeadersAuthnResource(self)) | |
|
83 | config.add_view( | |
|
84 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
|
85 | attr='settings_get', | |
|
86 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
87 | request_method='GET', | |
|
88 | route_name='auth_home', | |
|
89 | context=HeadersAuthnResource) | |
|
90 | config.add_view( | |
|
91 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
|
92 | attr='settings_post', | |
|
93 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
94 | request_method='POST', | |
|
95 | route_name='auth_home', | |
|
96 | context=HeadersAuthnResource) | |
|
97 | ||
|
98 | def get_display_name(self): | |
|
99 | return _('Headers') | |
|
100 | ||
|
101 | def get_settings_schema(self): | |
|
102 | return HeadersSettingsSchema() | |
|
103 | ||
|
104 | @hybrid_property | |
|
105 | def name(self): | |
|
106 | return 'headers' | |
|
107 | ||
|
108 | @property | |
|
109 | def is_headers_auth(self): | |
|
110 | return True | |
|
111 | ||
|
112 | def use_fake_password(self): | |
|
113 | return True | |
|
114 | ||
|
115 | def user_activation_state(self): | |
|
116 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] | |
|
117 | return 'hg.extern_activate.auto' in def_user_perms | |
|
118 | ||
|
119 | def _clean_username(self, username): | |
|
120 | # Removing realm and domain from username | |
|
121 | username = username.split('@')[0] | |
|
122 | username = username.rsplit('\\')[-1] | |
|
123 | return username | |
|
124 | ||
|
125 | def _get_username(self, environ, settings): | |
|
126 | username = None | |
|
127 | environ = environ or {} | |
|
128 | if not environ: | |
|
129 | log.debug('got empty environ: %s' % environ) | |
|
130 | ||
|
131 | settings = settings or {} | |
|
132 | if settings.get('header'): | |
|
133 | header = settings.get('header') | |
|
134 | username = environ.get(header) | |
|
135 | log.debug('extracted %s:%s' % (header, username)) | |
|
136 | ||
|
137 | # fallback mode | |
|
138 | if not username and settings.get('fallback_header'): | |
|
139 | header = settings.get('fallback_header') | |
|
140 | username = environ.get(header) | |
|
141 | log.debug('extracted %s:%s' % (header, username)) | |
|
142 | ||
|
143 | if username and str2bool(settings.get('clean_username')): | |
|
144 | log.debug('Received username `%s` from headers' % username) | |
|
145 | username = self._clean_username(username) | |
|
146 | log.debug('New cleanup user is:%s' % username) | |
|
147 | return username | |
|
148 | ||
|
149 | def get_user(self, username=None, **kwargs): | |
|
150 | """ | |
|
151 | Helper method for user fetching in plugins, by default it's using | |
|
152 | simple fetch by username, but this method can be custimized in plugins | |
|
153 | eg. headers auth plugin to fetch user by environ params | |
|
154 | :param username: username if given to fetch | |
|
155 | :param kwargs: extra arguments needed for user fetching. | |
|
156 | """ | |
|
157 | environ = kwargs.get('environ') or {} | |
|
158 | settings = kwargs.get('settings') or {} | |
|
159 | username = self._get_username(environ, settings) | |
|
160 | # we got the username, so use default method now | |
|
161 | return super(RhodeCodeAuthPlugin, self).get_user(username) | |
|
162 | ||
|
163 | def auth(self, userobj, username, password, settings, **kwargs): | |
|
164 | """ | |
|
165 | Get's the headers_auth username (or email). It tries to get username | |
|
166 | from REMOTE_USER if this plugin is enabled, if that fails | |
|
167 | it tries to get username from HTTP_X_FORWARDED_USER if fallback header | |
|
168 | is set. clean_username extracts the username from this data if it's | |
|
169 | having @ in it. | |
|
170 | Return None on failure. On success, return a dictionary of the form: | |
|
171 | ||
|
172 | see: RhodeCodeAuthPluginBase.auth_func_attrs | |
|
173 | ||
|
174 | :param userobj: | |
|
175 | :param username: | |
|
176 | :param password: | |
|
177 | :param settings: | |
|
178 | :param kwargs: | |
|
179 | """ | |
|
180 | environ = kwargs.get('environ') | |
|
181 | if not environ: | |
|
182 | log.debug('Empty environ data skipping...') | |
|
183 | return None | |
|
184 | ||
|
185 | if not userobj: | |
|
186 | userobj = self.get_user('', environ=environ, settings=settings) | |
|
187 | ||
|
188 | # we don't care passed username/password for headers auth plugins. | |
|
189 | # only way to log in is using environ | |
|
190 | username = None | |
|
191 | if userobj: | |
|
192 | username = getattr(userobj, 'username') | |
|
193 | ||
|
194 | if not username: | |
|
195 | # we don't have any objects in DB user doesn't exist extract | |
|
196 | # username from environ based on the settings | |
|
197 | username = self._get_username(environ, settings) | |
|
198 | ||
|
199 | # if cannot fetch username, it's a no-go for this plugin to proceed | |
|
200 | if not username: | |
|
201 | return None | |
|
202 | ||
|
203 | # old attrs fetched from RhodeCode database | |
|
204 | admin = getattr(userobj, 'admin', False) | |
|
205 | active = getattr(userobj, 'active', True) | |
|
206 | email = getattr(userobj, 'email', '') | |
|
207 | firstname = getattr(userobj, 'firstname', '') | |
|
208 | lastname = getattr(userobj, 'lastname', '') | |
|
209 | extern_type = getattr(userobj, 'extern_type', '') | |
|
210 | ||
|
211 | user_attrs = { | |
|
212 | 'username': username, | |
|
213 | 'firstname': safe_unicode(firstname or username), | |
|
214 | 'lastname': safe_unicode(lastname or ''), | |
|
215 | 'groups': [], | |
|
216 | 'email': email or '', | |
|
217 | 'admin': admin or False, | |
|
218 | 'active': active, | |
|
219 | 'active_from_extern': True, | |
|
220 | 'extern_name': username, | |
|
221 | 'extern_type': extern_type, | |
|
222 | } | |
|
223 | ||
|
224 | log.info('user `%s` authenticated correctly' % user_attrs['username']) | |
|
225 | return user_attrs |
@@ -0,0 +1,136 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2016-2016 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 | RhodeCode authentication token plugin for built in internal auth | |
|
23 | """ | |
|
24 | ||
|
25 | import logging | |
|
26 | ||
|
27 | from sqlalchemy.ext.hybrid import hybrid_property | |
|
28 | ||
|
29 | from rhodecode.translation import _ | |
|
30 | from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE | |
|
31 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
|
32 | from rhodecode.model.db import User, UserApiKeys | |
|
33 | ||
|
34 | ||
|
35 | log = logging.getLogger(__name__) | |
|
36 | ||
|
37 | ||
|
38 | def plugin_factory(plugin_id, *args, **kwds): | |
|
39 | plugin = RhodeCodeAuthPlugin(plugin_id) | |
|
40 | return plugin | |
|
41 | ||
|
42 | ||
|
43 | class RhodecodeAuthnResource(AuthnPluginResourceBase): | |
|
44 | pass | |
|
45 | ||
|
46 | ||
|
47 | class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase): | |
|
48 | """ | |
|
49 | Enables usage of authentication tokens for vcs operations. | |
|
50 | """ | |
|
51 | ||
|
52 | def includeme(self, config): | |
|
53 | config.add_authn_plugin(self) | |
|
54 | config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self)) | |
|
55 | config.add_view( | |
|
56 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
|
57 | attr='settings_get', | |
|
58 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
59 | request_method='GET', | |
|
60 | route_name='auth_home', | |
|
61 | context=RhodecodeAuthnResource) | |
|
62 | config.add_view( | |
|
63 | 'rhodecode.authentication.views.AuthnPluginViewBase', | |
|
64 | attr='settings_post', | |
|
65 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
66 | request_method='POST', | |
|
67 | route_name='auth_home', | |
|
68 | context=RhodecodeAuthnResource) | |
|
69 | ||
|
70 | def get_display_name(self): | |
|
71 | return _('Rhodecode Token Auth') | |
|
72 | ||
|
73 | @hybrid_property | |
|
74 | def name(self): | |
|
75 | return "authtoken" | |
|
76 | ||
|
77 | def user_activation_state(self): | |
|
78 | def_user_perms = User.get_default_user().AuthUser.permissions['global'] | |
|
79 | return 'hg.register.auto_activate' in def_user_perms | |
|
80 | ||
|
81 | def allows_authentication_from( | |
|
82 | self, user, allows_non_existing_user=True, | |
|
83 | allowed_auth_plugins=None, allowed_auth_sources=None): | |
|
84 | """ | |
|
85 | Custom method for this auth that doesn't accept empty users. And also | |
|
86 | allows rhodecode and authtoken extern_type to auth with this. But only | |
|
87 | via vcs mode | |
|
88 | """ | |
|
89 | # only this and rhodecode plugins can use this type | |
|
90 | from rhodecode.authentication.plugins import auth_rhodecode | |
|
91 | allowed_auth_plugins = [ | |
|
92 | self.name, auth_rhodecode.RhodeCodeAuthPlugin.name] | |
|
93 | # only for vcs operations | |
|
94 | allowed_auth_sources = [VCS_TYPE] | |
|
95 | ||
|
96 | return super(RhodeCodeAuthPlugin, self).allows_authentication_from( | |
|
97 | user, allows_non_existing_user=False, | |
|
98 | allowed_auth_plugins=allowed_auth_plugins, | |
|
99 | allowed_auth_sources=allowed_auth_sources) | |
|
100 | ||
|
101 | def auth(self, userobj, username, password, settings, **kwargs): | |
|
102 | if not userobj: | |
|
103 | log.debug('userobj was:%s skipping' % (userobj, )) | |
|
104 | return None | |
|
105 | ||
|
106 | user_attrs = { | |
|
107 | "username": userobj.username, | |
|
108 | "firstname": userobj.firstname, | |
|
109 | "lastname": userobj.lastname, | |
|
110 | "groups": [], | |
|
111 | "email": userobj.email, | |
|
112 | "admin": userobj.admin, | |
|
113 | "active": userobj.active, | |
|
114 | "active_from_extern": userobj.active, | |
|
115 | "extern_name": userobj.user_id, | |
|
116 | "extern_type": userobj.extern_type, | |
|
117 | } | |
|
118 | ||
|
119 | log.debug('Authenticating user with args %s', user_attrs) | |
|
120 | if userobj.active: | |
|
121 | role = UserApiKeys.ROLE_VCS | |
|
122 | active_tokens = [x.api_key for x in | |
|
123 | User.extra_valid_auth_tokens(userobj, role=role)] | |
|
124 | if userobj.username == username and password in active_tokens: | |
|
125 | log.info( | |
|
126 | 'user `%s` successfully authenticated via %s', | |
|
127 | user_attrs['username'], self.name) | |
|
128 | return user_attrs | |
|
129 | log.error( | |
|
130 | 'user `%s` failed to authenticate via %s, reason: bad or ' | |
|
131 | 'inactive token.', username, self.name) | |
|
132 | else: | |
|
133 | log.warning( | |
|
134 | 'user `%s` failed to authenticate via %s, reason: account not ' | |
|
135 | 'active.', username, self.name) | |
|
136 | return None |
@@ -0,0 +1,42 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2010-2016 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 | def generate_jsroutes_content(jsroutes): | |
|
22 | statements = [] | |
|
23 | for url_name, url, fields in jsroutes: | |
|
24 | statements.append( | |
|
25 | "pyroutes.register('%s', '%s', %s);" % (url_name, url, fields)) | |
|
26 | return u''' | |
|
27 | /****************************************************************************** | |
|
28 | * * | |
|
29 | * DO NOT CHANGE THIS FILE MANUALLY * | |
|
30 | * * | |
|
31 | * * | |
|
32 | * This file is automatically generated when the app starts up. * | |
|
33 | * * | |
|
34 | * To add a route here pass jsroute=True to the route definition in the app * | |
|
35 | * * | |
|
36 | ******************************************************************************/ | |
|
37 | function registerRCRoutes() { | |
|
38 | // routes registration | |
|
39 | %s | |
|
40 | } | |
|
41 | ''' % '\n '.join(statements) | |
|
42 |
@@ -0,0 +1,31 b'' | |||
|
1 | # Copyright (C) 2016-2016 RhodeCode GmbH | |
|
2 | # | |
|
3 | # This program is free software: you can redistribute it and/or modify | |
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |
|
5 | # (only), as published by the Free Software Foundation. | |
|
6 | # | |
|
7 | # This program is distributed in the hope that it will be useful, | |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
10 | # GNU General Public License for more details. | |
|
11 | # | |
|
12 | # You should have received a copy of the GNU Affero General Public License | |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
|
14 | # | |
|
15 | # This program is dual-licensed. If you wish to learn more about the | |
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
18 | ||
|
19 | from zope.interface import implementer | |
|
20 | from rhodecode.interfaces import IUserRegistered | |
|
21 | ||
|
22 | ||
|
23 | @implementer(IUserRegistered) | |
|
24 | class UserRegistered(object): | |
|
25 | """ | |
|
26 | An instance of this class is emitted as an :term:`event` whenever a user | |
|
27 | account is registered. | |
|
28 | """ | |
|
29 | def __init__(self, user, session): | |
|
30 | self.user = user | |
|
31 | self.session = session |
@@ -0,0 +1,28 b'' | |||
|
1 | # Copyright (C) 2016-2016 RhodeCode GmbH | |
|
2 | # | |
|
3 | # This program is free software: you can redistribute it and/or modify | |
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |
|
5 | # (only), as published by the Free Software Foundation. | |
|
6 | # | |
|
7 | # This program is distributed in the hope that it will be useful, | |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
10 | # GNU General Public License for more details. | |
|
11 | # | |
|
12 | # You should have received a copy of the GNU Affero General Public License | |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
|
14 | # | |
|
15 | # This program is dual-licensed. If you wish to learn more about the | |
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
18 | ||
|
19 | from zope.interface import Attribute, Interface | |
|
20 | ||
|
21 | ||
|
22 | class IUserRegistered(Interface): | |
|
23 | """ | |
|
24 | An event type that is emitted whenever a new user registers a user | |
|
25 | account. | |
|
26 | """ | |
|
27 | user = Attribute('The user object.') | |
|
28 | session = Attribute('The session while processing the register form post.') |
@@ -0,0 +1,30 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2016-2016 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 strip_whitespace(value): | |
|
23 | """ | |
|
24 | Removes leading/trailing whitespace, newlines, and tabs from the value. | |
|
25 | Implements the `colander.interface.Preparer` interface. | |
|
26 | """ | |
|
27 | if isinstance(value, basestring): | |
|
28 | return value.strip(' \t\n\r') | |
|
29 | else: | |
|
30 | return value |
@@ -0,0 +1,80 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | import logging | |
|
4 | ||
|
5 | from sqlalchemy.orm.attributes import flag_modified | |
|
6 | ||
|
7 | from rhodecode.lib.dbmigrate.versions import _reset_base | |
|
8 | from rhodecode.model import init_model_encryption, meta | |
|
9 | ||
|
10 | log = logging.getLogger(__name__) | |
|
11 | ||
|
12 | ||
|
13 | def upgrade(migrate_engine): | |
|
14 | """ | |
|
15 | Upgrade operations go here. | |
|
16 | Don't create your own engine; bind migrate_engine to your metadata | |
|
17 | """ | |
|
18 | _reset_base(migrate_engine) | |
|
19 | from rhodecode.lib.dbmigrate.schema import db_3_7_0_0 | |
|
20 | init_model_encryption(db_3_7_0_0) | |
|
21 | fixups(db_3_7_0_0, meta.Session) | |
|
22 | ||
|
23 | ||
|
24 | def downgrade(migrate_engine): | |
|
25 | pass | |
|
26 | ||
|
27 | ||
|
28 | AUTH_PLUGINS_SETTING = "auth_plugins" | |
|
29 | ||
|
30 | PLUGIN_RENAME_MAP = { | |
|
31 | 'egg:rhodecode-enterprise-ce#container': 'egg:rhodecode-enterprise-ce#headers', | |
|
32 | } | |
|
33 | ||
|
34 | SETTINGS_RENAME_MAP = { | |
|
35 | 'auth_container_cache_ttl': 'auth_headers_cache_ttl', | |
|
36 | 'auth_container_clean_username': 'auth_headers_clean_username', | |
|
37 | 'auth_container_enabled': 'auth_headers_enabled', | |
|
38 | 'auth_container_fallback_header': 'auth_headers_fallback_header', | |
|
39 | 'auth_container_header': 'auth_headers_header', | |
|
40 | } | |
|
41 | ||
|
42 | ||
|
43 | def rename_plugins(models, Session): | |
|
44 | query = models.RhodeCodeSetting.query().filter( | |
|
45 | models.RhodeCodeSetting.app_settings_name == AUTH_PLUGINS_SETTING) | |
|
46 | plugin_setting = query.scalar() | |
|
47 | plugins = plugin_setting.app_settings_value | |
|
48 | ||
|
49 | new_plugins = [] | |
|
50 | ||
|
51 | for plugin_id in plugins: | |
|
52 | new_plugin_id = PLUGIN_RENAME_MAP.get(plugin_id, None) | |
|
53 | if new_plugin_id: | |
|
54 | new_plugins.append(new_plugin_id) | |
|
55 | else: | |
|
56 | new_plugins.append(plugin_id) | |
|
57 | ||
|
58 | plugin_setting.app_settings_value = ','.join(new_plugins) | |
|
59 | ||
|
60 | log.info("Rename of auth plugin IDs") | |
|
61 | log.info("Original setting value: %s", plugins) | |
|
62 | log.info("New setting value: %s", new_plugins) | |
|
63 | ||
|
64 | ||
|
65 | def rename_plugin_settings(models, Session): | |
|
66 | for old_name, new_name in SETTINGS_RENAME_MAP.items(): | |
|
67 | query = models.RhodeCodeSetting.query().filter( | |
|
68 | models.RhodeCodeSetting.app_settings_name == old_name) | |
|
69 | setting = query.scalar() | |
|
70 | if setting: | |
|
71 | setting.app_settings_name = new_name | |
|
72 | log.info( | |
|
73 | 'Rename of plugin setting "%s" to "%s"', old_name, new_name) | |
|
74 | ||
|
75 | ||
|
76 | def fixups(models, Session): | |
|
77 | rename_plugins(models, Session) | |
|
78 | rename_plugin_settings(models, Session) | |
|
79 | ||
|
80 | Session().commit() |
@@ -0,0 +1,57 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | import logging | |
|
4 | ||
|
5 | from rhodecode.lib.dbmigrate.versions import _reset_base | |
|
6 | from rhodecode.model import init_model_encryption, meta | |
|
7 | ||
|
8 | log = logging.getLogger(__name__) | |
|
9 | ||
|
10 | ||
|
11 | def upgrade(migrate_engine): | |
|
12 | """ | |
|
13 | Upgrade operations go here. | |
|
14 | Don't create your own engine; bind migrate_engine to your metadata | |
|
15 | """ | |
|
16 | _reset_base(migrate_engine) | |
|
17 | from rhodecode.lib.dbmigrate.schema import db_3_7_0_0 | |
|
18 | init_model_encryption(db_3_7_0_0) | |
|
19 | fixups(db_3_7_0_0, meta.Session) | |
|
20 | ||
|
21 | ||
|
22 | def downgrade(migrate_engine): | |
|
23 | pass | |
|
24 | ||
|
25 | ||
|
26 | AUTH_PLUGINS_SETTING = "auth_plugins" | |
|
27 | ||
|
28 | PLUGIN_RENAME_MAP = { | |
|
29 | 'egg:rhodecode-enterprise-ee#token': 'egg:rhodecode-enterprise-ce#token', | |
|
30 | } | |
|
31 | ||
|
32 | ||
|
33 | def rename_plugins(models, Session): | |
|
34 | query = models.RhodeCodeSetting.query().filter( | |
|
35 | models.RhodeCodeSetting.app_settings_name == AUTH_PLUGINS_SETTING) | |
|
36 | plugin_setting = query.scalar() | |
|
37 | plugins = plugin_setting.app_settings_value | |
|
38 | ||
|
39 | new_plugins = [] | |
|
40 | ||
|
41 | for plugin_id in plugins: | |
|
42 | new_plugin_id = PLUGIN_RENAME_MAP.get(plugin_id, None) | |
|
43 | if new_plugin_id: | |
|
44 | new_plugins.append(new_plugin_id) | |
|
45 | else: | |
|
46 | new_plugins.append(plugin_id) | |
|
47 | ||
|
48 | plugin_setting.app_settings_value = ','.join(new_plugins) | |
|
49 | ||
|
50 | log.info("Rename of auth plugin IDs") | |
|
51 | log.info("Original setting value: %s", plugins) | |
|
52 | log.info("New setting value: %s", new_plugins) | |
|
53 | ||
|
54 | ||
|
55 | def fixups(models, Session): | |
|
56 | rename_plugins(models, Session) | |
|
57 | Session().commit() |
@@ -0,0 +1,55 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | import logging | |
|
4 | from collections import namedtuple | |
|
5 | ||
|
6 | from rhodecode.lib.dbmigrate.versions import _reset_base | |
|
7 | from rhodecode.model import init_model_encryption, meta | |
|
8 | ||
|
9 | log = logging.getLogger(__name__) | |
|
10 | ||
|
11 | ||
|
12 | def upgrade(migrate_engine): | |
|
13 | """ | |
|
14 | Upgrade operations go here. | |
|
15 | Don't create your own engine; bind migrate_engine to your metadata | |
|
16 | """ | |
|
17 | _reset_base(migrate_engine) | |
|
18 | from rhodecode.lib.dbmigrate.schema import db_3_7_0_0 | |
|
19 | init_model_encryption(db_3_7_0_0) | |
|
20 | fixups(db_3_7_0_0, meta.Session) | |
|
21 | ||
|
22 | ||
|
23 | def downgrade(migrate_engine): | |
|
24 | pass | |
|
25 | ||
|
26 | ||
|
27 | AUTH_PLUGINS_SETTING = "auth_plugins" | |
|
28 | ||
|
29 | EXTERN_TYPE_RENAME_MAP = { | |
|
30 | 'container': 'headers', | |
|
31 | } | |
|
32 | ||
|
33 | # Only used for logging purposes. | |
|
34 | RenameExternTypeOperation = namedtuple( | |
|
35 | 'RenameExternTypeOperation', ['user', 'old', 'new']) | |
|
36 | ||
|
37 | ||
|
38 | def fixups(models, Session): | |
|
39 | operations = [] | |
|
40 | ||
|
41 | # Rename the extern_type attribute | |
|
42 | query = models.User.query().filter( | |
|
43 | models.User.extern_type.in_(EXTERN_TYPE_RENAME_MAP.keys())) | |
|
44 | for user in query: | |
|
45 | old = user.extern_type | |
|
46 | new = EXTERN_TYPE_RENAME_MAP[old] | |
|
47 | user.extern_type = new | |
|
48 | Session.add(user) | |
|
49 | operations.append(RenameExternTypeOperation(user, old, new)) | |
|
50 | ||
|
51 | log.info("Migration of users 'extern_type' attribute.") | |
|
52 | for op in operations: | |
|
53 | log.info("%s", op) | |
|
54 | ||
|
55 | Session().commit() |
@@ -0,0 +1,44 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2016-2016 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.config.routing import ADMIN_PREFIX | |
|
23 | ||
|
24 | ||
|
25 | def includeme(config): | |
|
26 | ||
|
27 | config.add_route( | |
|
28 | name='login', | |
|
29 | pattern=ADMIN_PREFIX + '/login') | |
|
30 | config.add_route( | |
|
31 | name='logout', | |
|
32 | pattern=ADMIN_PREFIX + '/logout') | |
|
33 | config.add_route( | |
|
34 | name='register', | |
|
35 | pattern=ADMIN_PREFIX + '/register') | |
|
36 | config.add_route( | |
|
37 | name='reset_password', | |
|
38 | pattern=ADMIN_PREFIX + '/password_reset') | |
|
39 | config.add_route( | |
|
40 | name='reset_password_confirmation', | |
|
41 | pattern=ADMIN_PREFIX + '/password_reset_confirmation') | |
|
42 | ||
|
43 | # Scan module for configuration decorators. | |
|
44 | config.scan() |
@@ -0,0 +1,337 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2016-2016 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 datetime | |
|
22 | import formencode | |
|
23 | import logging | |
|
24 | import urlparse | |
|
25 | ||
|
26 | from pylons import url | |
|
27 | from pyramid.httpexceptions import HTTPFound | |
|
28 | from pyramid.view import view_config | |
|
29 | from recaptcha.client.captcha import submit | |
|
30 | ||
|
31 | from rhodecode.authentication.base import authenticate, HTTP_TYPE | |
|
32 | from rhodecode.events import UserRegistered | |
|
33 | from rhodecode.lib.auth import ( | |
|
34 | AuthUser, HasPermissionAnyDecorator, CSRFRequired) | |
|
35 | from rhodecode.lib.base import get_ip_addr | |
|
36 | from rhodecode.lib.exceptions import UserCreationError | |
|
37 | from rhodecode.lib.utils2 import safe_str | |
|
38 | from rhodecode.model.db import User | |
|
39 | from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm | |
|
40 | from rhodecode.model.login_session import LoginSession | |
|
41 | from rhodecode.model.meta import Session | |
|
42 | from rhodecode.model.settings import SettingsModel | |
|
43 | from rhodecode.model.user import UserModel | |
|
44 | from rhodecode.translation import _ | |
|
45 | ||
|
46 | ||
|
47 | log = logging.getLogger(__name__) | |
|
48 | ||
|
49 | ||
|
50 | def _store_user_in_session(session, username, remember=False): | |
|
51 | user = User.get_by_username(username, case_insensitive=True) | |
|
52 | auth_user = AuthUser(user.user_id) | |
|
53 | auth_user.set_authenticated() | |
|
54 | cs = auth_user.get_cookie_store() | |
|
55 | session['rhodecode_user'] = cs | |
|
56 | user.update_lastlogin() | |
|
57 | Session().commit() | |
|
58 | ||
|
59 | # If they want to be remembered, update the cookie | |
|
60 | if remember: | |
|
61 | _year = (datetime.datetime.now() + | |
|
62 | datetime.timedelta(seconds=60 * 60 * 24 * 365)) | |
|
63 | session._set_cookie_expires(_year) | |
|
64 | ||
|
65 | session.save() | |
|
66 | ||
|
67 | log.info('user %s is now authenticated and stored in ' | |
|
68 | 'session, session attrs %s', username, cs) | |
|
69 | ||
|
70 | # dumps session attrs back to cookie | |
|
71 | session._update_cookie_out() | |
|
72 | # we set new cookie | |
|
73 | headers = None | |
|
74 | if session.request['set_cookie']: | |
|
75 | # send set-cookie headers back to response to update cookie | |
|
76 | headers = [('Set-Cookie', session.request['cookie_out'])] | |
|
77 | return headers | |
|
78 | ||
|
79 | ||
|
80 | def get_came_from(request): | |
|
81 | came_from = safe_str(request.GET.get('came_from', '')) | |
|
82 | parsed = urlparse.urlparse(came_from) | |
|
83 | allowed_schemes = ['http', 'https'] | |
|
84 | if parsed.scheme and parsed.scheme not in allowed_schemes: | |
|
85 | log.error('Suspicious URL scheme detected %s for url %s' % | |
|
86 | (parsed.scheme, parsed)) | |
|
87 | came_from = url('home') | |
|
88 | elif parsed.netloc and request.host != parsed.netloc: | |
|
89 | log.error('Suspicious NETLOC detected %s for url %s server url ' | |
|
90 | 'is: %s' % (parsed.netloc, parsed, request.host)) | |
|
91 | came_from = url('home') | |
|
92 | elif any(bad_str in parsed.path for bad_str in ('\r', '\n')): | |
|
93 | log.error('Header injection detected `%s` for url %s server url ' % | |
|
94 | (parsed.path, parsed)) | |
|
95 | came_from = url('home') | |
|
96 | ||
|
97 | return came_from or url('home') | |
|
98 | ||
|
99 | ||
|
100 | class LoginView(object): | |
|
101 | ||
|
102 | def __init__(self, context, request): | |
|
103 | self.request = request | |
|
104 | self.context = context | |
|
105 | self.session = request.session | |
|
106 | self._rhodecode_user = request.user | |
|
107 | ||
|
108 | def _get_template_context(self): | |
|
109 | return { | |
|
110 | 'came_from': get_came_from(self.request), | |
|
111 | 'defaults': {}, | |
|
112 | 'errors': {}, | |
|
113 | } | |
|
114 | ||
|
115 | @view_config( | |
|
116 | route_name='login', request_method='GET', | |
|
117 | renderer='rhodecode:templates/login.html') | |
|
118 | def login(self): | |
|
119 | came_from = get_came_from(self.request) | |
|
120 | user = self.request.user | |
|
121 | ||
|
122 | # redirect if already logged in | |
|
123 | if user.is_authenticated and not user.is_default and user.ip_allowed: | |
|
124 | raise HTTPFound(came_from) | |
|
125 | ||
|
126 | # check if we use headers plugin, and try to login using it. | |
|
127 | try: | |
|
128 | log.debug('Running PRE-AUTH for headers based authentication') | |
|
129 | auth_info = authenticate( | |
|
130 | '', '', self.request.environ, HTTP_TYPE, skip_missing=True) | |
|
131 | if auth_info: | |
|
132 | headers = _store_user_in_session( | |
|
133 | self.session, auth_info.get('username')) | |
|
134 | raise HTTPFound(came_from, headers=headers) | |
|
135 | except UserCreationError as e: | |
|
136 | log.error(e) | |
|
137 | self.session.flash(e, queue='error') | |
|
138 | ||
|
139 | return self._get_template_context() | |
|
140 | ||
|
141 | @view_config( | |
|
142 | route_name='login', request_method='POST', | |
|
143 | renderer='rhodecode:templates/login.html') | |
|
144 | def login_post(self): | |
|
145 | came_from = get_came_from(self.request) | |
|
146 | session = self.request.session | |
|
147 | login_form = LoginForm()() | |
|
148 | ||
|
149 | try: | |
|
150 | session.invalidate() | |
|
151 | form_result = login_form.to_python(self.request.params) | |
|
152 | # form checks for username/password, now we're authenticated | |
|
153 | headers = _store_user_in_session( | |
|
154 | self.session, | |
|
155 | username=form_result['username'], | |
|
156 | remember=form_result['remember']) | |
|
157 | raise HTTPFound(came_from, headers=headers) | |
|
158 | except formencode.Invalid as errors: | |
|
159 | defaults = errors.value | |
|
160 | # remove password from filling in form again | |
|
161 | del defaults['password'] | |
|
162 | render_ctx = self._get_template_context() | |
|
163 | render_ctx.update({ | |
|
164 | 'errors': errors.error_dict, | |
|
165 | 'defaults': defaults, | |
|
166 | }) | |
|
167 | return render_ctx | |
|
168 | ||
|
169 | except UserCreationError as e: | |
|
170 | # headers auth or other auth functions that create users on | |
|
171 | # the fly can throw this exception signaling that there's issue | |
|
172 | # with user creation, explanation should be provided in | |
|
173 | # Exception itself | |
|
174 | session.flash(e, queue='error') | |
|
175 | return self._get_template_context() | |
|
176 | ||
|
177 | @CSRFRequired() | |
|
178 | @view_config(route_name='logout', request_method='POST') | |
|
179 | def logout(self): | |
|
180 | LoginSession().destroy_user_session() | |
|
181 | return HTTPFound(url('home')) | |
|
182 | ||
|
183 | @HasPermissionAnyDecorator( | |
|
184 | 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') | |
|
185 | @view_config( | |
|
186 | route_name='register', request_method='GET', | |
|
187 | renderer='rhodecode:templates/register.html',) | |
|
188 | def register(self, defaults=None, errors=None): | |
|
189 | defaults = defaults or {} | |
|
190 | errors = errors or {} | |
|
191 | ||
|
192 | settings = SettingsModel().get_all_settings() | |
|
193 | captcha_public_key = settings.get('rhodecode_captcha_public_key') | |
|
194 | captcha_private_key = settings.get('rhodecode_captcha_private_key') | |
|
195 | captcha_active = bool(captcha_private_key) | |
|
196 | register_message = settings.get('rhodecode_register_message') or '' | |
|
197 | auto_active = 'hg.register.auto_activate' in User.get_default_user()\ | |
|
198 | .AuthUser.permissions['global'] | |
|
199 | ||
|
200 | render_ctx = self._get_template_context() | |
|
201 | render_ctx.update({ | |
|
202 | 'defaults': defaults, | |
|
203 | 'errors': errors, | |
|
204 | 'auto_active': auto_active, | |
|
205 | 'captcha_active': captcha_active, | |
|
206 | 'captcha_public_key': captcha_public_key, | |
|
207 | 'register_message': register_message, | |
|
208 | }) | |
|
209 | return render_ctx | |
|
210 | ||
|
211 | @view_config( | |
|
212 | route_name='register', request_method='POST', | |
|
213 | renderer='rhodecode:templates/register.html') | |
|
214 | def register_post(self): | |
|
215 | captcha_private_key = SettingsModel().get_setting_by_name( | |
|
216 | 'rhodecode_captcha_private_key') | |
|
217 | captcha_active = bool(captcha_private_key) | |
|
218 | auto_active = 'hg.register.auto_activate' in User.get_default_user()\ | |
|
219 | .AuthUser.permissions['global'] | |
|
220 | ||
|
221 | register_form = RegisterForm()() | |
|
222 | try: | |
|
223 | form_result = register_form.to_python(self.request.params) | |
|
224 | form_result['active'] = auto_active | |
|
225 | ||
|
226 | if captcha_active: | |
|
227 | response = submit( | |
|
228 | self.request.params.get('recaptcha_challenge_field'), | |
|
229 | self.request.params.get('recaptcha_response_field'), | |
|
230 | private_key=captcha_private_key, | |
|
231 | remoteip=get_ip_addr(self.request.environ)) | |
|
232 | if captcha_active and not response.is_valid: | |
|
233 | _value = form_result | |
|
234 | _msg = _('bad captcha') | |
|
235 | error_dict = {'recaptcha_field': _msg} | |
|
236 | raise formencode.Invalid(_msg, _value, None, | |
|
237 | error_dict=error_dict) | |
|
238 | ||
|
239 | new_user = UserModel().create_registration(form_result) | |
|
240 | event = UserRegistered(user=new_user, session=self.session) | |
|
241 | self.request.registry.notify(event) | |
|
242 | self.session.flash( | |
|
243 | _('You have successfully registered with RhodeCode'), | |
|
244 | queue='success') | |
|
245 | Session().commit() | |
|
246 | ||
|
247 | redirect_ro = self.request.route_path('login') | |
|
248 | raise HTTPFound(redirect_ro) | |
|
249 | ||
|
250 | except formencode.Invalid as errors: | |
|
251 | del errors.value['password'] | |
|
252 | del errors.value['password_confirmation'] | |
|
253 | return self.register( | |
|
254 | defaults=errors.value, errors=errors.error_dict) | |
|
255 | ||
|
256 | except UserCreationError as e: | |
|
257 | # container auth or other auth functions that create users on | |
|
258 | # the fly can throw this exception signaling that there's issue | |
|
259 | # with user creation, explanation should be provided in | |
|
260 | # Exception itself | |
|
261 | self.session.flash(e, queue='error') | |
|
262 | return self.register() | |
|
263 | ||
|
264 | @view_config( | |
|
265 | route_name='reset_password', request_method=('GET', 'POST'), | |
|
266 | renderer='rhodecode:templates/password_reset.html') | |
|
267 | def password_reset(self): | |
|
268 | settings = SettingsModel().get_all_settings() | |
|
269 | captcha_private_key = settings.get('rhodecode_captcha_private_key') | |
|
270 | captcha_active = bool(captcha_private_key) | |
|
271 | captcha_public_key = settings.get('rhodecode_captcha_public_key') | |
|
272 | ||
|
273 | render_ctx = { | |
|
274 | 'captcha_active': captcha_active, | |
|
275 | 'captcha_public_key': captcha_public_key, | |
|
276 | 'defaults': {}, | |
|
277 | 'errors': {}, | |
|
278 | } | |
|
279 | ||
|
280 | if self.request.POST: | |
|
281 | password_reset_form = PasswordResetForm()() | |
|
282 | try: | |
|
283 | form_result = password_reset_form.to_python( | |
|
284 | self.request.params) | |
|
285 | if captcha_active: | |
|
286 | response = submit( | |
|
287 | self.request.params.get('recaptcha_challenge_field'), | |
|
288 | self.request.params.get('recaptcha_response_field'), | |
|
289 | private_key=captcha_private_key, | |
|
290 | remoteip=get_ip_addr(self.request.environ)) | |
|
291 | if captcha_active and not response.is_valid: | |
|
292 | _value = form_result | |
|
293 | _msg = _('bad captcha') | |
|
294 | error_dict = {'recaptcha_field': _msg} | |
|
295 | raise formencode.Invalid(_msg, _value, None, | |
|
296 | error_dict=error_dict) | |
|
297 | ||
|
298 | # Generate reset URL and send mail. | |
|
299 | user_email = form_result['email'] | |
|
300 | user = User.get_by_email(user_email) | |
|
301 | password_reset_url = self.request.route_url( | |
|
302 | 'reset_password_confirmation', | |
|
303 | _query={'key': user.api_key}) | |
|
304 | UserModel().reset_password_link( | |
|
305 | form_result, password_reset_url) | |
|
306 | ||
|
307 | # Display success message and redirect. | |
|
308 | self.session.flash( | |
|
309 | _('Your password reset link was sent'), | |
|
310 | queue='success') | |
|
311 | return HTTPFound(self.request.route_path('login')) | |
|
312 | ||
|
313 | except formencode.Invalid as errors: | |
|
314 | render_ctx.update({ | |
|
315 | 'defaults': errors.value, | |
|
316 | 'errors': errors.error_dict, | |
|
317 | }) | |
|
318 | ||
|
319 | return render_ctx | |
|
320 | ||
|
321 | @view_config(route_name='reset_password_confirmation', | |
|
322 | request_method='GET') | |
|
323 | def password_reset_confirmation(self): | |
|
324 | if self.request.GET and self.request.GET.get('key'): | |
|
325 | try: | |
|
326 | user = User.get_by_auth_token(self.request.GET.get('key')) | |
|
327 | data = {'email': user.email} | |
|
328 | UserModel().reset_password(data) | |
|
329 | self.session.flash( | |
|
330 | _('Your password reset was successful, ' | |
|
331 | 'a new password has been sent to your email'), | |
|
332 | queue='success') | |
|
333 | except Exception as e: | |
|
334 | log.error(e) | |
|
335 | return HTTPFound(self.request.route_path('reset_password')) | |
|
336 | ||
|
337 | return HTTPFound(self.request.route_path('login')) |
@@ -0,0 +1,490 b'' | |||
|
1 | /*!*************************************************** | |
|
2 | * mark.js v6.1.0 | |
|
3 | * https://github.com/julmot/mark.js | |
|
4 | * Copyright (c) 2014–2016, Julian Motz | |
|
5 | * Released under the MIT license https://git.io/vwTVl | |
|
6 | *****************************************************/ | |
|
7 | ||
|
8 | "use strict"; | |
|
9 | ||
|
10 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | |
|
11 | ||
|
12 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | |
|
13 | ||
|
14 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; | |
|
15 | ||
|
16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | |
|
17 | ||
|
18 | (function (factory, window, document) { | |
|
19 | if (typeof define === "function" && define.amd) { | |
|
20 | define(["jquery"], function (jQuery) { | |
|
21 | return factory(window, document, jQuery); | |
|
22 | }); | |
|
23 | } else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object") { | |
|
24 | factory(window, document, require("jquery")); | |
|
25 | } else { | |
|
26 | factory(window, document, jQuery); | |
|
27 | } | |
|
28 | })(function (window, document, $) { | |
|
29 | var Mark = function () { | |
|
30 | function Mark(ctx) { | |
|
31 | _classCallCheck(this, Mark); | |
|
32 | ||
|
33 | this.ctx = ctx; | |
|
34 | } | |
|
35 | ||
|
36 | _createClass(Mark, [{ | |
|
37 | key: "log", | |
|
38 | value: function log(msg) { | |
|
39 | var level = arguments.length <= 1 || arguments[1] === undefined ? "debug" : arguments[1]; | |
|
40 | ||
|
41 | var log = this.opt.log; | |
|
42 | if (!this.opt.debug) { | |
|
43 | return; | |
|
44 | } | |
|
45 | if ((typeof log === "undefined" ? "undefined" : _typeof(log)) === "object" && typeof log[level] === "function") { | |
|
46 | log[level]("mark.js: " + msg); | |
|
47 | } | |
|
48 | } | |
|
49 | }, { | |
|
50 | key: "escapeStr", | |
|
51 | value: function escapeStr(str) { | |
|
52 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); | |
|
53 | } | |
|
54 | }, { | |
|
55 | key: "createRegExp", | |
|
56 | value: function createRegExp(str) { | |
|
57 | str = this.escapeStr(str); | |
|
58 | if (Object.keys(this.opt.synonyms).length) { | |
|
59 | str = this.createSynonymsRegExp(str); | |
|
60 | } | |
|
61 | if (this.opt.diacritics) { | |
|
62 | str = this.createDiacriticsRegExp(str); | |
|
63 | } | |
|
64 | str = this.createAccuracyRegExp(str); | |
|
65 | return str; | |
|
66 | } | |
|
67 | }, { | |
|
68 | key: "createSynonymsRegExp", | |
|
69 | value: function createSynonymsRegExp(str) { | |
|
70 | var syn = this.opt.synonyms; | |
|
71 | for (var index in syn) { | |
|
72 | if (syn.hasOwnProperty(index)) { | |
|
73 | var value = syn[index], | |
|
74 | k1 = this.escapeStr(index), | |
|
75 | k2 = this.escapeStr(value); | |
|
76 | str = str.replace(new RegExp("(" + k1 + "|" + k2 + ")", "gmi"), "(" + k1 + "|" + k2 + ")"); | |
|
77 | } | |
|
78 | } | |
|
79 | return str; | |
|
80 | } | |
|
81 | }, { | |
|
82 | key: "createDiacriticsRegExp", | |
|
83 | value: function createDiacriticsRegExp(str) { | |
|
84 | var dct = ["aÀÁÂÃÄÅàáâãäåĀāąĄ", "cÇçćĆčČ", "dđĐďĎ", "eÈÉÊËèéêëěĚĒēęĘ", "iÌÍÎÏìíîïĪī", "lłŁ", "nÑñňŇńŃ", "oÒÓÔÕÕÖØòóôõöøŌō", "rřŘ", "sŠšśŚ", "tťŤ", "uÙÚÛÜùúûüůŮŪū", "yŸÿýÝ", "zŽžżŻźŹ"]; | |
|
85 | var handled = []; | |
|
86 | str.split("").forEach(function (ch) { | |
|
87 | dct.every(function (dct) { | |
|
88 | if (dct.indexOf(ch) !== -1) { | |
|
89 | if (handled.indexOf(dct) > -1) { | |
|
90 | return false; | |
|
91 | } | |
|
92 | ||
|
93 | str = str.replace(new RegExp("[" + dct + "]", "gmi"), "[" + dct + "]"); | |
|
94 | handled.push(dct); | |
|
95 | } | |
|
96 | return true; | |
|
97 | }); | |
|
98 | }); | |
|
99 | return str; | |
|
100 | } | |
|
101 | }, { | |
|
102 | key: "createAccuracyRegExp", | |
|
103 | value: function createAccuracyRegExp(str) { | |
|
104 | switch (this.opt.accuracy) { | |
|
105 | case "partially": | |
|
106 | return "()(" + str + ")"; | |
|
107 | case "complementary": | |
|
108 | return "()(\\S*" + str + "\\S*)"; | |
|
109 | case "exactly": | |
|
110 | return "(^|\\s)(" + str + ")(?=\\s|$)"; | |
|
111 | } | |
|
112 | } | |
|
113 | }, { | |
|
114 | key: "getSeparatedKeywords", | |
|
115 | value: function getSeparatedKeywords(sv) { | |
|
116 | var _this = this; | |
|
117 | ||
|
118 | var stack = []; | |
|
119 | sv.forEach(function (kw) { | |
|
120 | if (!_this.opt.separateWordSearch) { | |
|
121 | if (kw.trim()) { | |
|
122 | stack.push(kw); | |
|
123 | } | |
|
124 | } else { | |
|
125 | kw.split(" ").forEach(function (kwSplitted) { | |
|
126 | if (kwSplitted.trim()) { | |
|
127 | stack.push(kwSplitted); | |
|
128 | } | |
|
129 | }); | |
|
130 | } | |
|
131 | }); | |
|
132 | return { | |
|
133 | "keywords": stack, | |
|
134 | "length": stack.length | |
|
135 | }; | |
|
136 | } | |
|
137 | }, { | |
|
138 | key: "getElements", | |
|
139 | value: function getElements() { | |
|
140 | var ctx = void 0, | |
|
141 | stack = []; | |
|
142 | if (typeof this.ctx === "undefined") { | |
|
143 | ctx = []; | |
|
144 | } else if (this.ctx instanceof HTMLElement) { | |
|
145 | ctx = [this.ctx]; | |
|
146 | } else if (Array.isArray(this.ctx)) { | |
|
147 | ctx = this.ctx; | |
|
148 | } else { | |
|
149 | ctx = Array.prototype.slice.call(this.ctx); | |
|
150 | } | |
|
151 | ctx.forEach(function (ctx) { | |
|
152 | stack.push(ctx); | |
|
153 | var childs = ctx.querySelectorAll("*"); | |
|
154 | if (childs.length) { | |
|
155 | stack = stack.concat(Array.prototype.slice.call(childs)); | |
|
156 | } | |
|
157 | }); | |
|
158 | if (!ctx.length) { | |
|
159 | this.log("Empty context", "warn"); | |
|
160 | } | |
|
161 | return { | |
|
162 | "elements": stack, | |
|
163 | "length": stack.length | |
|
164 | }; | |
|
165 | } | |
|
166 | }, { | |
|
167 | key: "matches", | |
|
168 | value: function matches(el, selector) { | |
|
169 | return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); | |
|
170 | } | |
|
171 | }, { | |
|
172 | key: "matchesFilter", | |
|
173 | value: function matchesFilter(el, exclM) { | |
|
174 | var _this2 = this; | |
|
175 | ||
|
176 | var remain = true; | |
|
177 | var fltr = this.opt.filter.concat(["script", "style", "title"]); | |
|
178 | if (!this.opt.iframes) { | |
|
179 | fltr = fltr.concat(["iframe"]); | |
|
180 | } | |
|
181 | if (exclM) { | |
|
182 | fltr = fltr.concat(["*[data-markjs='true']"]); | |
|
183 | } | |
|
184 | fltr.every(function (filter) { | |
|
185 | if (_this2.matches(el, filter)) { | |
|
186 | return remain = false; | |
|
187 | } | |
|
188 | return true; | |
|
189 | }); | |
|
190 | return !remain; | |
|
191 | } | |
|
192 | }, { | |
|
193 | key: "onIframeReady", | |
|
194 | value: function onIframeReady(ifr, successFn, errorFn) { | |
|
195 | try { | |
|
196 | (function () { | |
|
197 | var ifrWin = ifr.contentWindow, | |
|
198 | bl = "about:blank", | |
|
199 | compl = "complete"; | |
|
200 | var callCallback = function callCallback() { | |
|
201 | try { | |
|
202 | if (ifrWin.document === null) { | |
|
203 | throw new Error("iframe inaccessible"); | |
|
204 | } | |
|
205 | successFn(ifrWin.document); | |
|
206 | } catch (e) { | |
|
207 | errorFn(); | |
|
208 | } | |
|
209 | }; | |
|
210 | var isBlank = function isBlank() { | |
|
211 | var src = ifr.getAttribute("src").trim(), | |
|
212 | href = ifrWin.location.href; | |
|
213 | return href === bl && src !== bl && src; | |
|
214 | }; | |
|
215 | var observeOnload = function observeOnload() { | |
|
216 | var listener = function listener() { | |
|
217 | try { | |
|
218 | if (!isBlank()) { | |
|
219 | ifr.removeEventListener("load", listener); | |
|
220 | callCallback(); | |
|
221 | } | |
|
222 | } catch (e) { | |
|
223 | errorFn(); | |
|
224 | } | |
|
225 | }; | |
|
226 | ifr.addEventListener("load", listener); | |
|
227 | }; | |
|
228 | if (ifrWin.document.readyState === compl) { | |
|
229 | if (isBlank()) { | |
|
230 | observeOnload(); | |
|
231 | } else { | |
|
232 | callCallback(); | |
|
233 | } | |
|
234 | } else { | |
|
235 | observeOnload(); | |
|
236 | } | |
|
237 | })(); | |
|
238 | } catch (e) { | |
|
239 | errorFn(); | |
|
240 | } | |
|
241 | } | |
|
242 | }, { | |
|
243 | key: "forEachElementInIframe", | |
|
244 | value: function forEachElementInIframe(ifr, cb) { | |
|
245 | var _this3 = this; | |
|
246 | ||
|
247 | var end = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2]; | |
|
248 | ||
|
249 | var open = 0; | |
|
250 | var checkEnd = function checkEnd() { | |
|
251 | if (--open < 1) { | |
|
252 | end(); | |
|
253 | } | |
|
254 | }; | |
|
255 | this.onIframeReady(ifr, function (con) { | |
|
256 | var stack = Array.prototype.slice.call(con.querySelectorAll("*")); | |
|
257 | if ((open = stack.length) === 0) { | |
|
258 | checkEnd(); | |
|
259 | } | |
|
260 | stack.forEach(function (el) { | |
|
261 | if (el.tagName.toLowerCase() === "iframe") { | |
|
262 | (function () { | |
|
263 | var j = 0; | |
|
264 | _this3.forEachElementInIframe(el, function (iel, len) { | |
|
265 | cb(iel, len); | |
|
266 | if (len - 1 === j) { | |
|
267 | checkEnd(); | |
|
268 | } | |
|
269 | j++; | |
|
270 | }, checkEnd); | |
|
271 | })(); | |
|
272 | } else { | |
|
273 | cb(el, stack.length); | |
|
274 | checkEnd(); | |
|
275 | } | |
|
276 | }); | |
|
277 | }, function () { | |
|
278 | var src = ifr.getAttribute("src"); | |
|
279 | _this3.log("iframe '" + src + "' could not be accessed", "warn"); | |
|
280 | checkEnd(); | |
|
281 | }); | |
|
282 | } | |
|
283 | }, { | |
|
284 | key: "forEachElement", | |
|
285 | value: function forEachElement(cb) { | |
|
286 | var _this4 = this; | |
|
287 | ||
|
288 | var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1]; | |
|
289 | var exclM = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2]; | |
|
290 | ||
|
291 | var _getElements = this.getElements(); | |
|
292 | ||
|
293 | var stack = _getElements.elements; | |
|
294 | var open = _getElements.length; | |
|
295 | ||
|
296 | var checkEnd = function checkEnd() { | |
|
297 | if (--open === 0) { | |
|
298 | end(); | |
|
299 | } | |
|
300 | }; | |
|
301 | checkEnd(++open); | |
|
302 | stack.forEach(function (el) { | |
|
303 | if (!_this4.matchesFilter(el, exclM)) { | |
|
304 | if (el.tagName.toLowerCase() === "iframe") { | |
|
305 | _this4.forEachElementInIframe(el, function (iel) { | |
|
306 | if (!_this4.matchesFilter(iel, exclM)) { | |
|
307 | cb(iel); | |
|
308 | } | |
|
309 | }, checkEnd); | |
|
310 | return; | |
|
311 | } else { | |
|
312 | cb(el); | |
|
313 | } | |
|
314 | } | |
|
315 | checkEnd(); | |
|
316 | }); | |
|
317 | } | |
|
318 | }, { | |
|
319 | key: "forEachNode", | |
|
320 | value: function forEachNode(cb) { | |
|
321 | var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1]; | |
|
322 | ||
|
323 | this.forEachElement(function (n) { | |
|
324 | for (n = n.firstChild; n; n = n.nextSibling) { | |
|
325 | if (n.nodeType === 3 && n.textContent.trim()) { | |
|
326 | cb(n); | |
|
327 | } | |
|
328 | } | |
|
329 | }, end); | |
|
330 | } | |
|
331 | }, { | |
|
332 | key: "wrapMatches", | |
|
333 | value: function wrapMatches(node, regex, custom, cb) { | |
|
334 | var hEl = !this.opt.element ? "mark" : this.opt.element, | |
|
335 | index = custom ? 0 : 2; | |
|
336 | var match = void 0; | |
|
337 | while ((match = regex.exec(node.textContent)) !== null) { | |
|
338 | var pos = match.index; | |
|
339 | if (!custom) { | |
|
340 | pos += match[index - 1].length; | |
|
341 | } | |
|
342 | var startNode = node.splitText(pos); | |
|
343 | ||
|
344 | node = startNode.splitText(match[index].length); | |
|
345 | if (startNode.parentNode !== null) { | |
|
346 | var repl = document.createElement(hEl); | |
|
347 | repl.setAttribute("data-markjs", "true"); | |
|
348 | if (this.opt.className) { | |
|
349 | repl.setAttribute("class", this.opt.className); | |
|
350 | } | |
|
351 | repl.textContent = match[index]; | |
|
352 | startNode.parentNode.replaceChild(repl, startNode); | |
|
353 | cb(repl); | |
|
354 | } | |
|
355 | regex.lastIndex = 0; | |
|
356 | } | |
|
357 | } | |
|
358 | }, { | |
|
359 | key: "unwrapMatches", | |
|
360 | value: function unwrapMatches(node) { | |
|
361 | var parent = node.parentNode; | |
|
362 | var docFrag = document.createDocumentFragment(); | |
|
363 | while (node.firstChild) { | |
|
364 | docFrag.appendChild(node.removeChild(node.firstChild)); | |
|
365 | } | |
|
366 | parent.replaceChild(docFrag, node); | |
|
367 | parent.normalize(); | |
|
368 | } | |
|
369 | }, { | |
|
370 | key: "markRegExp", | |
|
371 | value: function markRegExp(regexp, opt) { | |
|
372 | var _this5 = this; | |
|
373 | ||
|
374 | this.opt = opt; | |
|
375 | this.log("Searching with expression \"" + regexp + "\""); | |
|
376 | var found = false; | |
|
377 | var eachCb = function eachCb(element) { | |
|
378 | found = true; | |
|
379 | _this5.opt.each(element); | |
|
380 | }; | |
|
381 | this.forEachNode(function (node) { | |
|
382 | _this5.wrapMatches(node, regexp, true, eachCb); | |
|
383 | }, function () { | |
|
384 | if (!found) { | |
|
385 | _this5.opt.noMatch(regexp); | |
|
386 | } | |
|
387 | _this5.opt.complete(); | |
|
388 | _this5.opt.done(); | |
|
389 | }); | |
|
390 | } | |
|
391 | }, { | |
|
392 | key: "mark", | |
|
393 | value: function mark(sv, opt) { | |
|
394 | var _this6 = this; | |
|
395 | ||
|
396 | this.opt = opt; | |
|
397 | sv = typeof sv === "string" ? [sv] : sv; | |
|
398 | ||
|
399 | var _getSeparatedKeywords = this.getSeparatedKeywords(sv); | |
|
400 | ||
|
401 | var kwArr = _getSeparatedKeywords.keywords; | |
|
402 | var kwArrLen = _getSeparatedKeywords.length; | |
|
403 | ||
|
404 | if (kwArrLen === 0) { | |
|
405 | this.opt.complete(); | |
|
406 | this.opt.done(); | |
|
407 | } | |
|
408 | kwArr.forEach(function (kw) { | |
|
409 | var regex = new RegExp(_this6.createRegExp(kw), "gmi"), | |
|
410 | found = false; | |
|
411 | var eachCb = function eachCb(element) { | |
|
412 | found = true; | |
|
413 | _this6.opt.each(element); | |
|
414 | }; | |
|
415 | _this6.log("Searching with expression \"" + regex + "\""); | |
|
416 | _this6.forEachNode(function (node) { | |
|
417 | _this6.wrapMatches(node, regex, false, eachCb); | |
|
418 | }, function () { | |
|
419 | if (!found) { | |
|
420 | _this6.opt.noMatch(kw); | |
|
421 | } | |
|
422 | if (kwArr[kwArrLen - 1] === kw) { | |
|
423 | _this6.opt.complete(); | |
|
424 | _this6.opt.done(); | |
|
425 | } | |
|
426 | }); | |
|
427 | }); | |
|
428 | } | |
|
429 | }, { | |
|
430 | key: "unmark", | |
|
431 | value: function unmark(opt) { | |
|
432 | var _this7 = this; | |
|
433 | ||
|
434 | this.opt = opt; | |
|
435 | var sel = this.opt.element ? this.opt.element : "*"; | |
|
436 | sel += "[data-markjs]"; | |
|
437 | if (this.opt.className) { | |
|
438 | sel += "." + this.opt.className; | |
|
439 | } | |
|
440 | this.log("Removal selector \"" + sel + "\""); | |
|
441 | this.forEachElement(function (el) { | |
|
442 | if (_this7.matches(el, sel)) { | |
|
443 | _this7.unwrapMatches(el); | |
|
444 | } | |
|
445 | }, function () { | |
|
446 | _this7.opt.complete(); | |
|
447 | _this7.opt.done(); | |
|
448 | }, false); | |
|
449 | } | |
|
450 | }, { | |
|
451 | key: "opt", | |
|
452 | set: function set(val) { | |
|
453 | this._opt = _extends({}, { | |
|
454 | "element": "", | |
|
455 | "className": "", | |
|
456 | "filter": [], | |
|
457 | "iframes": false, | |
|
458 | "separateWordSearch": true, | |
|
459 | "diacritics": true, | |
|
460 | "synonyms": {}, | |
|
461 | "accuracy": "partially", | |
|
462 | "each": function each() {}, | |
|
463 | "noMatch": function noMatch() {}, | |
|
464 | "done": function done() {}, | |
|
465 | "complete": function complete() {}, | |
|
466 | "debug": false, | |
|
467 | "log": window.console | |
|
468 | }, val); | |
|
469 | }, | |
|
470 | get: function get() { | |
|
471 | return this._opt; | |
|
472 | } | |
|
473 | }]); | |
|
474 | ||
|
475 | return Mark; | |
|
476 | }(); | |
|
477 | ||
|
478 | $.fn.mark = function (sv, opt) { | |
|
479 | new Mark(this).mark(sv, opt); | |
|
480 | return this; | |
|
481 | }; | |
|
482 | $.fn.markRegExp = function (regexp, opt) { | |
|
483 | new Mark(this).markRegExp(regexp, opt); | |
|
484 | return this; | |
|
485 | }; | |
|
486 | $.fn.unmark = function (opt) { | |
|
487 | new Mark(this).unmark(opt); | |
|
488 | return this; | |
|
489 | }; | |
|
490 | }, window, document); |
@@ -0,0 +1,26 b'' | |||
|
1 | // # Copyright (C) 2010-2016 RhodeCode GmbH | |
|
2 | // # | |
|
3 | // # This program is free software: you can redistribute it and/or modify | |
|
4 | // # it under the terms of the GNU Affero General Public License, version 3 | |
|
5 | // # (only), as published by the Free Software Foundation. | |
|
6 | // # | |
|
7 | // # This program is distributed in the hope that it will be useful, | |
|
8 | // # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
9 | // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
10 | // # GNU General Public License for more details. | |
|
11 | // # | |
|
12 | // # You should have received a copy of the GNU Affero General Public License | |
|
13 | // # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
|
14 | // # | |
|
15 | // # This program is dual-licensed. If you wish to learn more about the | |
|
16 | // # RhodeCode Enterprise Edition, including its added features, Support services, | |
|
17 | // # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
18 | ||
|
19 | ||
|
20 | /* | |
|
21 | * Deferred functions that must run before any rhodecode javascript go here | |
|
22 | */ | |
|
23 | ||
|
24 | registerRCRoutes(); | |
|
25 | ||
|
26 | // TODO: move i18n here |
@@ -0,0 +1,22 b'' | |||
|
1 | # Copyright (C) 2016-2016 RhodeCode GmbH | |
|
2 | # | |
|
3 | # This program is free software: you can redistribute it and/or modify | |
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |
|
5 | # (only), as published by the Free Software Foundation. | |
|
6 | # | |
|
7 | # This program is distributed in the hope that it will be useful, | |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
10 | # GNU General Public License for more details. | |
|
11 | # | |
|
12 | # You should have received a copy of the GNU Affero General Public License | |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
|
14 | # | |
|
15 | # This program is dual-licensed. If you wish to learn more about the | |
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
18 | ||
|
19 | from pyramid.i18n import TranslationStringFactory | |
|
20 | ||
|
21 | # Create a translation string factory for the 'rhodecode' domain. | |
|
22 | _ = TranslationStringFactory('rhodecode') |
@@ -1,5 +1,5 b'' | |||
|
1 | 1 | [bumpversion] |
|
2 |
current_version = 4. |
|
|
2 | current_version = 4.1.0 | |
|
3 | 3 | message = release: Bump version {current_version} to {new_version} |
|
4 | 4 | |
|
5 | 5 | [bumpversion:file:rhodecode/VERSION] |
@@ -27,12 +27,13 b' module.exports = function(grunt) {' | |||
|
27 | 27 | '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js', |
|
28 | 28 | '<%= dirs.js.src %>/plugins/jquery.autocomplete.js', |
|
29 | 29 | '<%= dirs.js.src %>/plugins/jquery.debounce.js', |
|
30 | '<%= dirs.js.src %>/plugins/jquery.mark.js', | |
|
30 | 31 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', |
|
31 | 32 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', |
|
32 | 33 | |
|
33 | 34 | // Select2 |
|
34 | 35 | '<%= dirs.js.src %>/select2/select2.js', |
|
35 | ||
|
36 | ||
|
36 | 37 | // Code-mirror |
|
37 | 38 | '<%= dirs.js.src %>/codemirror/codemirror.js', |
|
38 | 39 | '<%= dirs.js.src %>/codemirror/codemirror_loadmode.js', |
@@ -59,7 +60,7 b' module.exports = function(grunt) {' | |||
|
59 | 60 | '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js', |
|
60 | 61 | |
|
61 | 62 | // Rhodecode components |
|
62 |
'<%= dirs.js.src %>/rhodecode/ |
|
|
63 | '<%= dirs.js.src %>/rhodecode/init.js', | |
|
63 | 64 | '<%= dirs.js.src %>/rhodecode/codemirror.js', |
|
64 | 65 | '<%= dirs.js.src %>/rhodecode/comments.js', |
|
65 | 66 | '<%= dirs.js.src %>/rhodecode/constants.js', |
@@ -34,9 +34,10 b' pdebug = false' | |||
|
34 | 34 | host = 127.0.0.1 |
|
35 | 35 | port = 5000 |
|
36 | 36 | |
|
37 | ########################## | |
|
38 | ## WAITRESS WSGI SERVER ## | |
|
39 | ########################## | |
|
37 | ################################## | |
|
38 | ## WAITRESS WSGI SERVER ## | |
|
39 | ## Recommended for Development ## | |
|
40 | ################################## | |
|
40 | 41 | use = egg:waitress#main |
|
41 | 42 | ## number of worker threads |
|
42 | 43 | threads = 5 |
@@ -56,7 +57,7 b' asyncore_use_poll = true' | |||
|
56 | 57 | ## when this option is set to more than one worker, recommended |
|
57 | 58 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers |
|
58 | 59 | ## The `instance_id = *` must be set in the [app:main] section below |
|
59 |
#workers = |
|
|
60 | #workers = 2 | |
|
60 | 61 | ## number of threads for each of the worker, must be set to 1 for gevent |
|
61 | 62 | ## generally recommened to be at 1 |
|
62 | 63 | #threads = 1 |
@@ -71,7 +72,7 b' asyncore_use_poll = true' | |||
|
71 | 72 | ## restarted, could prevent memory leaks |
|
72 | 73 | #max_requests = 1000 |
|
73 | 74 | #max_requests_jitter = 30 |
|
74 |
## am |
|
|
75 | ## amount of time a worker can spend with handling a request before it | |
|
75 | 76 | ## gets killed and restarted. Set to 6hrs |
|
76 | 77 | #timeout = 21600 |
|
77 | 78 | |
@@ -199,6 +200,21 b' default_encoding = UTF-8' | |||
|
199 | 200 | ## all running rhodecode instances. Leave empty if you don't use it |
|
200 | 201 | instance_id = |
|
201 | 202 | |
|
203 | ## Fallback authentication plugin. Set this to a plugin ID to force the usage | |
|
204 | ## of an authentication plugin also if it is disabled by it's settings. | |
|
205 | ## This could be useful if you are unable to log in to the system due to broken | |
|
206 | ## authentication settings. Then you can enable e.g. the internal rhodecode auth | |
|
207 | ## module to log in again and fix the settings. | |
|
208 | ## | |
|
209 | ## Available builtin plugin IDs (hash is part of the ID): | |
|
210 | ## egg:rhodecode-enterprise-ce#rhodecode | |
|
211 | ## egg:rhodecode-enterprise-ce#pam | |
|
212 | ## egg:rhodecode-enterprise-ce#ldap | |
|
213 | ## egg:rhodecode-enterprise-ce#jasig_cas | |
|
214 | ## egg:rhodecode-enterprise-ce#headers | |
|
215 | ## egg:rhodecode-enterprise-ce#crowd | |
|
216 | #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode | |
|
217 | ||
|
202 | 218 | ## alternative return HTTP header for failed authentication. Default HTTP |
|
203 | 219 | ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with |
|
204 | 220 | ## handling that causing a series of failed authentication calls. |
@@ -316,7 +332,7 b' beaker.cache.repo_cache_long.expire = 25' | |||
|
316 | 332 | #################################### |
|
317 | 333 | |
|
318 | 334 | ## .session.type is type of storage options for the session, current allowed |
|
319 | ## types are file, ext:memcached, ext:database, and memory(default). | |
|
335 | ## types are file, ext:memcached, ext:database, and memory (default). | |
|
320 | 336 | beaker.session.type = file |
|
321 | 337 | beaker.session.data_dir = %(here)s/data/sessions/data |
|
322 | 338 | |
@@ -356,12 +372,17 b' beaker.session.auto = false' | |||
|
356 | 372 | ################################### |
|
357 | 373 | ## SEARCH INDEXING CONFIGURATION ## |
|
358 | 374 | ################################### |
|
375 | ## Full text search indexer is available in rhodecode-tools under | |
|
376 | ## `rhodecode-tools index` command | |
|
359 | 377 | |
|
378 | # WHOOSH Backend, doesn't require additional services to run | |
|
379 | # it works good with few dozen repos | |
|
360 | 380 | search.module = rhodecode.lib.index.whoosh |
|
361 | 381 | search.location = %(here)s/data/index |
|
362 | 382 | |
|
383 | ||
|
363 | 384 | ################################### |
|
364 | ## ERROR AND LOG HANDLING SYSTEM ## | |
|
385 | ## APPENLIGHT CONFIG ## | |
|
365 | 386 | ################################### |
|
366 | 387 | |
|
367 | 388 | ## Appenlight is tailored to work with RhodeCode, see |
@@ -372,7 +393,7 b' appenlight = false' | |||
|
372 | 393 | |
|
373 | 394 | appenlight.server_url = https://api.appenlight.com |
|
374 | 395 | appenlight.api_key = YOUR_API_KEY |
|
375 |
|
|
|
396 | #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5 | |
|
376 | 397 | |
|
377 | 398 | # used for JS client |
|
378 | 399 | appenlight.api_public_key = YOUR_API_PUBLIC_KEY |
@@ -462,16 +483,26 b' sqlalchemy.db1.convert_unicode = true' | |||
|
462 | 483 | ################## |
|
463 | 484 | vcs.server.enable = true |
|
464 | 485 | vcs.server = localhost:9900 |
|
465 | # Available protocols: pyro4, http | |
|
466 | vcs.server.protocol = pyro4 | |
|
467 | 486 | |
|
468 | # available impl: | |
|
469 | # vcsserver.scm_app (EE only, for testing), | |
|
470 | # rhodecode.lib.middleware.utils.scm_app_http | |
|
471 | # pyro4 | |
|
487 | ## Web server connectivity protocol, responsible for web based VCS operatations | |
|
488 | ## Available protocols are: | |
|
489 | ## `pyro4` - using pyro4 server | |
|
490 | ## `http` - using http-rpc backend | |
|
491 | #vcs.server.protocol = http | |
|
492 | ||
|
493 | ## Push/Pull operations protocol, available options are: | |
|
494 | ## `pyro4` - using pyro4 server | |
|
495 | ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended | |
|
496 | ## `vcsserver.scm_app` - internal app (EE only) | |
|
472 | 497 | #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http |
|
473 | 498 | |
|
499 | ## Push/Pull operations hooks protocol, available options are: | |
|
500 | ## `pyro4` - using pyro4 server | |
|
501 | ## `http` - using http-rpc backend | |
|
502 | #vcs.hooks.protocol = http | |
|
503 | ||
|
474 | 504 | vcs.server.log_level = debug |
|
505 | ## Start VCSServer with this instance as a subprocess, usefull for development | |
|
475 | 506 | vcs.start_server = true |
|
476 | 507 | vcs.backends = hg, git, svn |
|
477 | 508 | vcs.connection_timeout = 3600 |
@@ -34,46 +34,47 b' pdebug = false' | |||
|
34 | 34 | host = 127.0.0.1 |
|
35 | 35 | port = 5000 |
|
36 | 36 | |
|
37 | ########################## | |
|
38 | ## WAITRESS WSGI SERVER ## | |
|
39 | ########################## | |
|
40 | use = egg:waitress#main | |
|
37 | ################################## | |
|
38 | ## WAITRESS WSGI SERVER ## | |
|
39 | ## Recommended for Development ## | |
|
40 | ################################## | |
|
41 | #use = egg:waitress#main | |
|
41 | 42 | ## number of worker threads |
|
42 | threads = 5 | |
|
43 | #threads = 5 | |
|
43 | 44 | ## MAX BODY SIZE 100GB |
|
44 | max_request_body_size = 107374182400 | |
|
45 | #max_request_body_size = 107374182400 | |
|
45 | 46 | ## Use poll instead of select, fixes file descriptors limits problems. |
|
46 | 47 | ## May not work on old windows systems. |
|
47 | asyncore_use_poll = true | |
|
48 | #asyncore_use_poll = true | |
|
48 | 49 | |
|
49 | 50 | |
|
50 | 51 | ########################## |
|
51 | 52 | ## GUNICORN WSGI SERVER ## |
|
52 | 53 | ########################## |
|
53 | 54 | ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini> |
|
54 |
|
|
|
55 | use = egg:gunicorn#main | |
|
55 | 56 | ## Sets the number of process workers. You must set `instance_id = *` |
|
56 | 57 | ## when this option is set to more than one worker, recommended |
|
57 | 58 | ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers |
|
58 | 59 | ## The `instance_id = *` must be set in the [app:main] section below |
|
59 |
|
|
|
60 | workers = 2 | |
|
60 | 61 | ## number of threads for each of the worker, must be set to 1 for gevent |
|
61 | 62 | ## generally recommened to be at 1 |
|
62 | 63 | #threads = 1 |
|
63 | 64 | ## process name |
|
64 |
|
|
|
65 | proc_name = rhodecode | |
|
65 | 66 | ## type of worker class, one of sync, gevent |
|
66 | 67 | ## recommended for bigger setup is using of of other than sync one |
|
67 |
|
|
|
68 | worker_class = sync | |
|
68 | 69 | ## The maximum number of simultaneous clients. Valid only for Gevent |
|
69 | 70 | #worker_connections = 10 |
|
70 | 71 | ## max number of requests that worker will handle before being gracefully |
|
71 | 72 | ## restarted, could prevent memory leaks |
|
72 |
|
|
|
73 |
|
|
|
74 |
## am |
|
|
73 | max_requests = 1000 | |
|
74 | max_requests_jitter = 30 | |
|
75 | ## amount of time a worker can spend with handling a request before it | |
|
75 | 76 | ## gets killed and restarted. Set to 6hrs |
|
76 |
|
|
|
77 | timeout = 21600 | |
|
77 | 78 | |
|
78 | 79 | |
|
79 | 80 | ## prefix middleware for RhodeCode, disables force_https flag. |
@@ -173,6 +174,21 b' default_encoding = UTF-8' | |||
|
173 | 174 | ## all running rhodecode instances. Leave empty if you don't use it |
|
174 | 175 | instance_id = |
|
175 | 176 | |
|
177 | ## Fallback authentication plugin. Set this to a plugin ID to force the usage | |
|
178 | ## of an authentication plugin also if it is disabled by it's settings. | |
|
179 | ## This could be useful if you are unable to log in to the system due to broken | |
|
180 | ## authentication settings. Then you can enable e.g. the internal rhodecode auth | |
|
181 | ## module to log in again and fix the settings. | |
|
182 | ## | |
|
183 | ## Available builtin plugin IDs (hash is part of the ID): | |
|
184 | ## egg:rhodecode-enterprise-ce#rhodecode | |
|
185 | ## egg:rhodecode-enterprise-ce#pam | |
|
186 | ## egg:rhodecode-enterprise-ce#ldap | |
|
187 | ## egg:rhodecode-enterprise-ce#jasig_cas | |
|
188 | ## egg:rhodecode-enterprise-ce#headers | |
|
189 | ## egg:rhodecode-enterprise-ce#crowd | |
|
190 | #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode | |
|
191 | ||
|
176 | 192 | ## alternative return HTTP header for failed authentication. Default HTTP |
|
177 | 193 | ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with |
|
178 | 194 | ## handling that causing a series of failed authentication calls. |
@@ -290,7 +306,7 b' beaker.cache.repo_cache_long.expire = 25' | |||
|
290 | 306 | #################################### |
|
291 | 307 | |
|
292 | 308 | ## .session.type is type of storage options for the session, current allowed |
|
293 | ## types are file, ext:memcached, ext:database, and memory(default). | |
|
309 | ## types are file, ext:memcached, ext:database, and memory (default). | |
|
294 | 310 | beaker.session.type = file |
|
295 | 311 | beaker.session.data_dir = %(here)s/data/sessions/data |
|
296 | 312 | |
@@ -304,7 +320,7 b' beaker.session.data_dir = %(here)s/data/' | |||
|
304 | 320 | |
|
305 | 321 | beaker.session.key = rhodecode |
|
306 | 322 | beaker.session.secret = production-rc-uytcxaz |
|
307 |
|
|
|
323 | beaker.session.lock_dir = %(here)s/data/sessions/lock | |
|
308 | 324 | |
|
309 | 325 | ## Secure encrypted cookie. Requires AES and AES python libraries |
|
310 | 326 | ## you must disable beaker.session.secret to use this |
@@ -330,12 +346,17 b' beaker.session.auto = false' | |||
|
330 | 346 | ################################### |
|
331 | 347 | ## SEARCH INDEXING CONFIGURATION ## |
|
332 | 348 | ################################### |
|
349 | ## Full text search indexer is available in rhodecode-tools under | |
|
350 | ## `rhodecode-tools index` command | |
|
333 | 351 | |
|
352 | # WHOOSH Backend, doesn't require additional services to run | |
|
353 | # it works good with few dozen repos | |
|
334 | 354 | search.module = rhodecode.lib.index.whoosh |
|
335 | 355 | search.location = %(here)s/data/index |
|
336 | 356 | |
|
357 | ||
|
337 | 358 | ################################### |
|
338 | ## ERROR AND LOG HANDLING SYSTEM ## | |
|
359 | ## APPENLIGHT CONFIG ## | |
|
339 | 360 | ################################### |
|
340 | 361 | |
|
341 | 362 | ## Appenlight is tailored to work with RhodeCode, see |
@@ -346,7 +367,7 b' appenlight = false' | |||
|
346 | 367 | |
|
347 | 368 | appenlight.server_url = https://api.appenlight.com |
|
348 | 369 | appenlight.api_key = YOUR_API_KEY |
|
349 |
|
|
|
370 | #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5 | |
|
350 | 371 | |
|
351 | 372 | # used for JS client |
|
352 | 373 | appenlight.api_public_key = YOUR_API_PUBLIC_KEY |
@@ -401,11 +422,6 b' appenlight.log_namespace_blacklist =' | |||
|
401 | 422 | set debug = false |
|
402 | 423 | |
|
403 | 424 | |
|
404 | ############## | |
|
405 | ## STYLING ## | |
|
406 | ############## | |
|
407 | debug_style = false | |
|
408 | ||
|
409 | 425 | ######################################################### |
|
410 | 426 | ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ### |
|
411 | 427 | ######################################################### |
@@ -436,16 +452,26 b' sqlalchemy.db1.convert_unicode = true' | |||
|
436 | 452 | ################## |
|
437 | 453 | vcs.server.enable = true |
|
438 | 454 | vcs.server = localhost:9900 |
|
439 | # Available protocols: pyro4, http | |
|
440 | vcs.server.protocol = pyro4 | |
|
441 | 455 | |
|
442 | # available impl: | |
|
443 | # vcsserver.scm_app (EE only, for testing), | |
|
444 | # rhodecode.lib.middleware.utils.scm_app_http | |
|
445 | # pyro4 | |
|
456 | ## Web server connectivity protocol, responsible for web based VCS operatations | |
|
457 | ## Available protocols are: | |
|
458 | ## `pyro4` - using pyro4 server | |
|
459 | ## `http` - using http-rpc backend | |
|
460 | #vcs.server.protocol = http | |
|
461 | ||
|
462 | ## Push/Pull operations protocol, available options are: | |
|
463 | ## `pyro4` - using pyro4 server | |
|
464 | ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended | |
|
465 | ## `vcsserver.scm_app` - internal app (EE only) | |
|
446 | 466 | #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http |
|
447 | 467 | |
|
468 | ## Push/Pull operations hooks protocol, available options are: | |
|
469 | ## `pyro4` - using pyro4 server | |
|
470 | ## `http` - using http-rpc backend | |
|
471 | #vcs.hooks.protocol = http | |
|
472 | ||
|
448 | 473 | vcs.server.log_level = info |
|
474 | ## Start VCSServer with this instance as a subprocess, usefull for development | |
|
449 | 475 | vcs.start_server = false |
|
450 | 476 | vcs.backends = hg, git, svn |
|
451 | 477 | vcs.connection_timeout = 3600 |
@@ -85,7 +85,7 b' let' | |||
|
85 | 85 | pythonLocalOverrides = self: super: { |
|
86 | 86 | rhodecode-enterprise-ce = |
|
87 | 87 | let |
|
88 |
version = |
|
|
88 | version = builtins.readFile ./rhodecode/VERSION; | |
|
89 | 89 | linkNodeModules = '' |
|
90 | 90 | echo "Link node packages" |
|
91 | 91 | # TODO: check if this adds stuff as a dependency, closure size |
@@ -119,7 +119,9 b' let' | |||
|
119 | 119 | # TODO: johbo: Make a nicer way to expose the parts. Maybe |
|
120 | 120 | # pkgs/default.nix? |
|
121 | 121 | passthru = { |
|
122 | inherit myPythonPackagesUnfix; | |
|
122 | inherit | |
|
123 | pythonLocalOverrides | |
|
124 | myPythonPackagesUnfix; | |
|
123 | 125 | pythonPackages = self; |
|
124 | 126 | }; |
|
125 | 127 | |
@@ -160,6 +162,7 b' let' | |||
|
160 | 162 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ |
|
161 | 163 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ |
|
162 | 164 | ln -s ${self.PasteScript}/bin/paster $out/bin/ |
|
165 | ln -s ${self.pyramid}/bin/* $out/bin/ #*/ | |
|
163 | 166 | |
|
164 | 167 | # rhodecode-tools |
|
165 | 168 | # TODO: johbo: re-think this. Do the tools import anything from enterprise? |
@@ -169,6 +172,7 b' let' | |||
|
169 | 172 | for file in $out/bin/*; do #*/ |
|
170 | 173 | wrapProgram $file \ |
|
171 | 174 | --prefix PYTHONPATH : $PYTHONPATH \ |
|
175 | --prefix PATH : $PATH \ | |
|
172 | 176 | --set PYTHONHASHSEED random |
|
173 | 177 | done |
|
174 | 178 |
@@ -9,24 +9,24 b' Here is a sample configuration file for ' | |||
|
9 | 9 | ServerName hg.myserver.com |
|
10 | 10 | ServerAlias hg.myserver.com |
|
11 | 11 | |
|
12 |
## uncomment root directive if you want to serve static files by |
|
|
13 | ## requires static_files = false in .ini file | |
|
14 |
DocumentRoot /path/to/installation |
|
|
12 | ## uncomment root directive if you want to serve static files by | |
|
13 | ## Apache requires static_files = false in .ini file | |
|
14 | #DocumentRoot /path/to/rhodecode/installation/public | |
|
15 | 15 | |
|
16 | 16 | <Proxy *> |
|
17 | 17 | Order allow,deny |
|
18 | 18 | Allow from all |
|
19 | 19 | </Proxy> |
|
20 | 20 | |
|
21 |
# |
|
|
22 | #Directive to properly generate url (clone url) for pylons | |
|
21 | ## Important ! | |
|
22 | ## Directive to properly generate url (clone url) for pylons | |
|
23 | 23 | ProxyPreserveHost On |
|
24 | 24 | |
|
25 |
# |
|
|
26 |
ProxyPass / http://127.0.0.1: |
|
|
27 |
ProxyPassReverse / http://127.0.0.1: |
|
|
25 | ## RhodeCode instance running | |
|
26 | ProxyPass / http://127.0.0.1:10002/ | |
|
27 | ProxyPassReverse / http://127.0.0.1:10002/ | |
|
28 | 28 | |
|
29 | #to enable https use line below | |
|
29 | ## to enable https use line below | |
|
30 | 30 | #SetEnvIf X-Url-Scheme https HTTPS=1 |
|
31 | 31 | |
|
32 | 32 | </VirtualHost> |
@@ -3,7 +3,15 b'' | |||
|
3 | 3 | Full-text Search |
|
4 | 4 | ---------------- |
|
5 | 5 | |
|
6 |
By default |RC |
|
|
6 | By default |RC| is configured to use `Whoosh`_ to index |repos| and | |
|
7 | provide full-text search. | |
|
8 | ||
|
9 | |RCE| also provides support for `Elasticsearch`_ as a backend for scalable | |
|
10 | search. See :ref:`enable-elasticsearch` for details. | |
|
11 | ||
|
12 | Indexing | |
|
13 | ^^^^^^^^ | |
|
14 | ||
|
7 | 15 | To run the indexer you need to use an |authtoken| with admin rights to all |
|
8 | 16 | |repos|. |
|
9 | 17 | |
@@ -232,4 +240,33 b' use the following example :file:`mapping' | |||
|
232 | 240 | max_filesize = 800MB |
|
233 | 241 | commit_parse_limit = 20000 |
|
234 | 242 | |
|
243 | .. _enable-elasticsearch: | |
|
244 | ||
|
245 | Enabling Elasticsearch | |
|
246 | ^^^^^^^^^^^^^^^^^^^^^^ | |
|
247 | ||
|
248 | 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The | |
|
249 | default location is | |
|
250 | :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` | |
|
251 | 2. Find the search configuration section: | |
|
252 | ||
|
253 | .. code-block:: ini | |
|
254 | ||
|
255 | ################################### | |
|
256 | ## SEARCH INDEXING CONFIGURATION ## | |
|
257 | ################################### | |
|
258 | ||
|
259 | search.module = rhodecode.lib.index.whoosh | |
|
260 | search.location = %(here)s/data/index | |
|
261 | ||
|
262 | and change it to: | |
|
263 | ||
|
264 | .. code-block:: ini | |
|
265 | ||
|
266 | search.module = rc_elasticsearch | |
|
267 | search.location = http://localhost:9200/ | |
|
268 | ||
|
269 | where ``search.location`` points to the elasticsearch server. | |
|
270 | ||
|
235 | 271 | .. _Whoosh: https://pypi.python.org/pypi/Whoosh/ |
|
272 | .. _Elasticsearch: https://www.elastic.co/ No newline at end of file |
@@ -7,11 +7,11 b' Use the following example to configure N' | |||
|
7 | 7 | |
|
8 | 8 | upstream rc { |
|
9 | 9 | |
|
10 |
server 127.0.0.1: |
|
|
10 | server 127.0.0.1:10002; | |
|
11 | 11 | |
|
12 | 12 | # add more instances for load balancing |
|
13 |
# server 127.0.0.1: |
|
|
14 |
# server 127.0.0.1: |
|
|
13 | # server 127.0.0.1:10003; | |
|
14 | # server 127.0.0.1:10004; | |
|
15 | 15 | } |
|
16 | 16 | |
|
17 | 17 | ## gist alias |
@@ -58,14 +58,15 b' Use the following example to configure N' | |||
|
58 | 58 | |
|
59 | 59 | ## uncomment root directive if you want to serve static files by nginx |
|
60 | 60 | ## requires static_files = false in .ini file |
|
61 |
# root /path/to/installation |
|
|
61 | # root /path/to/rhodecode/installation/public; | |
|
62 | 62 | |
|
63 | 63 | include /etc/nginx/proxy.conf; |
|
64 | location / { | |
|
65 | try_files $uri @rhode; | |
|
66 | } | |
|
64 | ||
|
65 | location / { | |
|
66 | try_files $uri @rhode; | |
|
67 | } | |
|
67 | 68 | |
|
68 | 69 | location @rhode { |
|
69 |
|
|
|
70 |
|
|
|
70 | proxy_pass http://rc; | |
|
71 | } | |
|
71 | 72 | } |
@@ -64,6 +64,14 b' performance is more important than CPU p' | |||
|
64 | 64 | environment handling 1000s of users and |repos| you should deploy on a 12+ |
|
65 | 65 | core 64GB RAM server. In short, the more RAM the better. |
|
66 | 66 | |
|
67 | ||
|
68 | For example: | |
|
69 | ||
|
70 | - for team of 1 - 5 active users you can run on 1GB RAM machine with 1CPU | |
|
71 | - above 250 active users, |RCM| needs at least 8GB of memory. | |
|
72 | Number of CPUs is less important, but recommended to have at least 2-3 CPUs | |
|
73 | ||
|
74 | ||
|
67 | 75 | .. _config-rce-files: |
|
68 | 76 | |
|
69 | 77 | Configuration Files |
@@ -23,6 +23,8 b" rst_epilog = '''" | |||
|
23 | 23 | .. |RCV| replace:: RhodeCode Enterprise |
|
24 | 24 | .. |RCM| replace:: RhodeCode Enterprise |
|
25 | 25 | .. |RCE| replace:: RhodeCode Enterprise |
|
26 | .. |RCCE| replace:: RhodeCode Community | |
|
27 | .. |RCEE| replace:: RhodeCode Enterprise | |
|
26 | 28 | .. |RCX| replace:: RhodeCode Extensions |
|
27 | 29 | .. |RCT| replace:: RhodeCode Tools |
|
28 | 30 | .. |RCEBOLD| replace:: **RhodeCode Enterprise** |
@@ -5,12 +5,12 b' Make Database Changes' | |||
|
5 | 5 | |
|
6 | 6 | .. important:: |
|
7 | 7 | |
|
8 |
If you do change the |repo| database that |RC |
|
|
8 | If you do change the |repo| database that |RCEE| uses, then you will need to | |
|
9 | 9 | upgrade the database, and also remap and rescan the |repos|. More detailed |
|
10 | 10 | information is available in the |
|
11 | 11 | :ref:`Alternative upgrade documentation <control:install-port>`. |
|
12 | 12 | |
|
13 |
If you need to change database connection details for a |RC |
|
|
13 | If you need to change database connection details for a |RCEE| instance, | |
|
14 | 14 | use the following steps: |
|
15 | 15 | |
|
16 | 16 | 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The |
@@ -17,10 +17,12 b' Quick Start Guide' | |||
|
17 | 17 | credentials during |RCE| installation. See the relevant database |
|
18 | 18 | documentation for more details. |
|
19 | 19 | |
|
20 |
To get |RC |
|
|
20 | To get |RCE| up and running, run through the below steps: | |
|
21 | 21 | |
|
22 | 22 | 1. Download the latest |RCC| installer from your `rhodecode.com`_ profile |
|
23 | page. If you don't have an account, sign up at `rhodecode.com/register`_. | |
|
23 | or main page. | |
|
24 | If you don't have an account, sign up at `rhodecode.com/register`_. | |
|
25 | ||
|
24 | 26 | 2. Run the |RCC| installer and accept the End User Licence using the |
|
25 | 27 | following example: |
|
26 | 28 | |
@@ -45,13 +47,18 b' 3. Install a VCS Server, and configure i' | |||
|
45 | 47 | Added process group vcsserver-1 |
|
46 | 48 | |
|
47 | 49 | |
|
48 |
4. Install |RCE|. If using MySQL or PostgreSQL, during |
|
|
49 |
asked for your database credentials, so have them at hand. |
|
|
50 | any for SQLite. | |
|
50 | 4. Install |RCEE| or |RCCE|. If using MySQL or PostgreSQL, during | |
|
51 | installation you'll be asked for your database credentials, so have them at hand. | |
|
52 | Mysql or Postgres needs to be running and a new database needs to be created. | |
|
53 | You don't need any credentials or to create a database for SQLite. | |
|
51 | 54 | |
|
52 | 55 | .. code-block:: bash |
|
53 | 56 | :emphasize-lines: 11-16 |
|
54 | 57 | |
|
58 | $ rccontrol install Community | |
|
59 | ||
|
60 | or | |
|
61 | ||
|
55 | 62 | $ rccontrol install Enterprise |
|
56 | 63 | |
|
57 | 64 | Username [admin]: username |
@@ -69,8 +76,8 b' 4. Install |RCE|. If using MySQL or Post' | |||
|
69 | 76 | Database password: somepassword |
|
70 | 77 | Database name: example-db-name |
|
71 | 78 | |
|
72 |
5. Check the status of your installation. You |RCE| instance runs |
|
|
73 | displayed in the status message. | |
|
79 | 5. Check the status of your installation. You |RCEE|/|RCCE| instance runs | |
|
80 | on the URL displayed in the status message. | |
|
74 | 81 | |
|
75 | 82 | .. code-block:: bash |
|
76 | 83 | |
@@ -79,13 +86,13 b' 5. Check the status of your installation' | |||
|
79 | 86 | - NAME: enterprise-1 |
|
80 | 87 | - STATUS: RUNNING |
|
81 | 88 | - TYPE: Enterprise |
|
82 |
- VERSION: |
|
|
89 | - VERSION: 4.1.0 | |
|
83 | 90 | - URL: http://127.0.0.1:10003 |
|
84 | 91 | |
|
85 | 92 | - NAME: vcsserver-1 |
|
86 | 93 | - STATUS: RUNNING |
|
87 | 94 | - TYPE: VCSServer |
|
88 |
- VERSION: |
|
|
95 | - VERSION: 4.1.0 | |
|
89 | 96 | - URL: http://127.0.0.1:10001 |
|
90 | 97 | |
|
91 | 98 | .. note:: |
@@ -37,6 +37,10 b' New Features' | |||
|
37 | 37 | Github, Twitter, Bitbucket and Google. It's possible now to use your |
|
38 | 38 | Google account to log in to RhodeCode and take advantage of things like 2FA. |
|
39 | 39 | |
|
40 | - Search: full text search now properly orders commits by date, and shows line | |
|
41 | numbers for file content search. | |
|
42 | ||
|
43 | ||
|
40 | 44 | Security |
|
41 | 45 | ^^^^^^^^ |
|
42 | 46 | |
@@ -46,8 +50,10 b' Security' | |||
|
46 | 50 | Performance |
|
47 | 51 | ^^^^^^^^^^^ |
|
48 | 52 | |
|
49 |
- Optimized admin pan |
|
|
53 | - Optimized admin panels to faster load large amount of data | |
|
50 | 54 | - Improved file tree loading speed |
|
55 | - New HTTP backend is ~10% faster, and doesn't require so many threads | |
|
56 | for vcsserver | |
|
51 | 57 | |
|
52 | 58 | |
|
53 | 59 | Fixes |
@@ -6,6 +6,10 b' Release Notes' | |||
|
6 | 6 | |RCE| 4.x Versions |
|
7 | 7 | ------------------ |
|
8 | 8 | |
|
9 | .. toctree:: | |
|
10 | :maxdepth: 1 | |
|
11 | ||
|
12 | release-notes-4.1.0.rst | |
|
9 | 13 | release-notes-4.0.1.rst |
|
10 | 14 | release-notes-4.0.0.rst |
|
11 | 15 |
@@ -1,13 +1,12 b'' | |||
|
1 | 1 | diff --git a/requirements.txt b/requirements.txt |
|
2 | 2 | --- a/requirements.txt |
|
3 | 3 | +++ b/requirements.txt |
|
4 | @@ -1,8 +1,8 @@ | |
|
5 | click==5.1 | |
|
6 | future==0.14.3 | |
|
4 | @@ -3,7 +3,7 @@future==0.14.3 | |
|
7 | 5 | six==1.9.0 |
|
8 | 6 | mako==1.0.1 |
|
9 | 7 | markupsafe==0.23 |
|
10 | 8 | -requests==2.5.1 |
|
11 | 9 | +requests |
|
10 | #responses | |
|
12 | 11 | whoosh==2.7.0 |
|
13 |
|
|
|
12 | elasticsearch==2.3.0 No newline at end of file |
@@ -21,6 +21,20 b' self: super: {' | |||
|
21 | 21 | ''; |
|
22 | 22 | }); |
|
23 | 23 | |
|
24 | gunicorn = super.gunicorn.override (attrs: { | |
|
25 | propagatedBuildInputs = attrs.propagatedBuildInputs ++ [ | |
|
26 | # johbo: futures is needed as long as we are on Python 2, otherwise | |
|
27 | # gunicorn explodes if used with multiple threads per worker. | |
|
28 | self.futures | |
|
29 | ]; | |
|
30 | }); | |
|
31 | ||
|
32 | ipython = super.ipython.override (attrs: { | |
|
33 | propagatedBuildInputs = attrs.propagatedBuildInputs ++ [ | |
|
34 | self.gnureadline | |
|
35 | ]; | |
|
36 | }); | |
|
37 | ||
|
24 | 38 | kombu = super.kombu.override (attrs: { |
|
25 | 39 | # The current version of kombu needs some patching to work with the |
|
26 | 40 | # other libs. Should be removed once we update celery and kombu. |
@@ -359,16 +359,6 b'' | |||
|
359 | 359 | md5 = "898bc87e54f278055b561316ba73e222"; |
|
360 | 360 | }; |
|
361 | 361 | }; |
|
362 | certifi = super.buildPythonPackage { | |
|
363 | name = "certifi-2016.2.28"; | |
|
364 | buildInputs = with self; []; | |
|
365 | doCheck = false; | |
|
366 | propagatedBuildInputs = with self; []; | |
|
367 | src = fetchurl { | |
|
368 | url = "https://pypi.python.org/packages/5c/f8/f6c54727c74579c6bbe5926f5deb9677c5810a33e11da58d1a4e2d09d041/certifi-2016.2.28.tar.gz"; | |
|
369 | md5 = "5d672aa766e1f773c75cfeccd02d3650"; | |
|
370 | }; | |
|
371 | }; | |
|
372 | 362 | click = super.buildPythonPackage { |
|
373 | 363 | name = "click-5.1"; |
|
374 | 364 | buildInputs = with self; []; |
@@ -490,13 +480,23 b'' | |||
|
490 | 480 | }; |
|
491 | 481 | }; |
|
492 | 482 | elasticsearch = super.buildPythonPackage { |
|
493 |
name = "elasticsearch- |
|
|
483 | name = "elasticsearch-2.3.0"; | |
|
494 | 484 | buildInputs = with self; []; |
|
495 | 485 | doCheck = false; |
|
496 | 486 | propagatedBuildInputs = with self; [urllib3]; |
|
497 | 487 | src = fetchurl { |
|
498 |
url = "https://pypi.python.org/packages/13 |
|
|
499 | md5 = "3550390baea1639479f79758d66ab032"; | |
|
488 | url = "https://pypi.python.org/packages/10/35/5fd52c5f0b0ee405ed4b5195e8bce44c5e041787680dc7b94b8071cac600/elasticsearch-2.3.0.tar.gz"; | |
|
489 | md5 = "2550f3b51629cf1ef9636608af92c340"; | |
|
490 | }; | |
|
491 | }; | |
|
492 | elasticsearch-dsl = super.buildPythonPackage { | |
|
493 | name = "elasticsearch-dsl-2.0.0"; | |
|
494 | buildInputs = with self; []; | |
|
495 | doCheck = false; | |
|
496 | propagatedBuildInputs = with self; [six python-dateutil elasticsearch]; | |
|
497 | src = fetchurl { | |
|
498 | url = "https://pypi.python.org/packages/4e/5d/e788ae8dbe2ff4d13426db0a027533386a5c276c77a2654dc0e2007ce04a/elasticsearch-dsl-2.0.0.tar.gz"; | |
|
499 | md5 = "4cdfec81bb35383dd3b7d02d7dc5ee68"; | |
|
500 | 500 | }; |
|
501 | 501 | }; |
|
502 | 502 | flake8 = super.buildPythonPackage { |
@@ -540,7 +540,7 b'' | |||
|
540 | 540 | }; |
|
541 | 541 | }; |
|
542 | 542 | gprof2dot = super.buildPythonPackage { |
|
543 | name = "gprof2dot-2015.12.1"; | |
|
543 | name = "gprof2dot-2015.12.01"; | |
|
544 | 544 | buildInputs = with self; []; |
|
545 | 545 | doCheck = false; |
|
546 | 546 | propagatedBuildInputs = with self; []; |
@@ -550,13 +550,13 b'' | |||
|
550 | 550 | }; |
|
551 | 551 | }; |
|
552 | 552 | greenlet = super.buildPythonPackage { |
|
553 |
name = "greenlet-0.4. |
|
|
553 | name = "greenlet-0.4.9"; | |
|
554 | 554 | buildInputs = with self; []; |
|
555 | 555 | doCheck = false; |
|
556 | 556 | propagatedBuildInputs = with self; []; |
|
557 | 557 | src = fetchurl { |
|
558 | url = "https://pypi.python.org/packages/7a/9f/a1a0d9bdf3203ae1502c5a8434fe89d323599d78a106985bc327351a69d4/greenlet-0.4.7.zip"; | |
|
559 | md5 = "c2333a8ff30fa75c5d5ec0e67b461086"; | |
|
558 | url = "https://pypi.python.org/packages/4e/3d/9d421539b74e33608b245092870156b2e171fb49f2b51390aa4641eecb4a/greenlet-0.4.9.zip"; | |
|
559 | md5 = "c6659cdb2a5e591723e629d2eef22e82"; | |
|
560 | 560 | }; |
|
561 | 561 | }; |
|
562 | 562 | gunicorn = super.buildPythonPackage { |
@@ -603,7 +603,7 b'' | |||
|
603 | 603 | name = "ipython-3.1.0"; |
|
604 | 604 | buildInputs = with self; []; |
|
605 | 605 | doCheck = false; |
|
606 |
propagatedBuildInputs = with self; [ |
|
|
606 | propagatedBuildInputs = with self; []; | |
|
607 | 607 | src = fetchurl { |
|
608 | 608 | url = "https://pypi.python.org/packages/06/91/120c0835254c120af89f066afaabf81289bc2726c1fc3ca0555df6882f58/ipython-3.1.0.tar.gz"; |
|
609 | 609 | md5 = "a749d90c16068687b0ec45a27e72ef8f"; |
@@ -799,16 +799,6 b'' | |||
|
799 | 799 | md5 = "47b4eac84118e2606658122104e62072"; |
|
800 | 800 | }; |
|
801 | 801 | }; |
|
802 | pyelasticsearch = super.buildPythonPackage { | |
|
803 | name = "pyelasticsearch-1.4"; | |
|
804 | buildInputs = with self; []; | |
|
805 | doCheck = false; | |
|
806 | propagatedBuildInputs = with self; [certifi elasticsearch urllib3 simplejson six]; | |
|
807 | src = fetchurl { | |
|
808 | url = "https://pypi.python.org/packages/2f/3a/7643cfcfc4cbdbb20ada800bbd54ac9705d0c047d7b8f8d5eeeb3047b4eb/pyelasticsearch-1.4.tar.gz"; | |
|
809 | md5 = "ed61ebb7b253364e55b4923d11e17049"; | |
|
810 | }; | |
|
811 | }; | |
|
812 | 802 | pyflakes = super.buildPythonPackage { |
|
813 | 803 | name = "pyflakes-0.8.1"; |
|
814 | 804 | buildInputs = with self; []; |
@@ -1050,20 +1040,20 b'' | |||
|
1050 | 1040 | }; |
|
1051 | 1041 | }; |
|
1052 | 1042 | rhodecode-enterprise-ce = super.buildPythonPackage { |
|
1053 |
name = "rhodecode-enterprise-ce-4.0 |
|
|
1043 | name = "rhodecode-enterprise-ce-4.1.0"; | |
|
1054 | 1044 | buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner]; |
|
1055 | 1045 | doCheck = true; |
|
1056 | 1046 | propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery colander decorator docutils gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 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 waitress zope.cachedescriptors psutil py-bcrypt]; |
|
1057 | 1047 | src = ./.; |
|
1058 | 1048 | }; |
|
1059 | 1049 | rhodecode-tools = super.buildPythonPackage { |
|
1060 |
name = "rhodecode-tools-0. |
|
|
1050 | name = "rhodecode-tools-0.8.3"; | |
|
1061 | 1051 | buildInputs = with self; []; |
|
1062 | 1052 | doCheck = false; |
|
1063 |
propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh |
|
|
1053 | propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh elasticsearch elasticsearch-dsl]; | |
|
1064 | 1054 | src = fetchurl { |
|
1065 |
url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0. |
|
|
1066 | md5 = "91daea803aaa264ce7a8213bc2220d4c"; | |
|
1055 | url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip"; | |
|
1056 | md5 = "9acdfd71b8ddf4056057065f37ab9ccb"; | |
|
1067 | 1057 | }; |
|
1068 | 1058 | }; |
|
1069 | 1059 | serpent = super.buildPythonPackage { |
@@ -11,7 +11,6 b' MySQL-python==1.2.5' | |||
|
11 | 11 | Paste==2.0.2 |
|
12 | 12 | PasteDeploy==1.5.2 |
|
13 | 13 | PasteScript==1.7.5 |
|
14 | pyelasticsearch==1.4 | |
|
15 | 14 | Pygments==2.0.2 |
|
16 | 15 | |
|
17 | 16 | # TODO: This version is not available on PyPI |
@@ -70,13 +69,14 b' flake8==2.4.1' | |||
|
70 | 69 | future==0.14.3 |
|
71 | 70 | futures==3.0.2 |
|
72 | 71 | gprof2dot==2015.12.1 |
|
73 |
greenlet==0.4. |
|
|
72 | greenlet==0.4.9 | |
|
74 | 73 | gunicorn==19.6.0 |
|
75 | 74 | |
|
76 | 75 | # TODO: Needs subvertpy and blows up without Subversion headers, |
|
77 | 76 | # actually we should not need this for Enterprise at all. |
|
78 | 77 | # hgsubversion==1.8.2 |
|
79 | 78 | |
|
79 | gnureadline==6.3.3 | |
|
80 | 80 | infrae.cache==1.0.1 |
|
81 | 81 | invoke==0.11.1 |
|
82 | 82 | ipdb==0.8 |
@@ -124,7 +124,7 b' pyzmq==14.6.0' | |||
|
124 | 124 | # TODO: This is not available in public |
|
125 | 125 | # rc-testdata==0.2.0 |
|
126 | 126 | |
|
127 |
https://code.rhodecode.com/rhodecode-tools-ce/archive/v0. |
|
|
127 | https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip#md5=9acdfd71b8ddf4056057065f37ab9ccb | |
|
128 | 128 | |
|
129 | 129 | |
|
130 | 130 | recaptcha-client==1.0.6 |
@@ -47,7 +47,7 b' CONFIG = {}' | |||
|
47 | 47 | EXTENSIONS = {} |
|
48 | 48 | |
|
49 | 49 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
50 |
__dbversion__ = 5 |
|
|
50 | __dbversion__ = 54 # defines current db version for migrations | |
|
51 | 51 | __platform__ = platform.system() |
|
52 | 52 | __license__ = 'AGPLv3, and Commercial License' |
|
53 | 53 | __author__ = 'RhodeCode GmbH' |
@@ -24,67 +24,81 b' import pytest' | |||
|
24 | 24 | from rhodecode.model.repo import RepoModel |
|
25 | 25 | from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN |
|
26 | 26 | from rhodecode.api.tests.utils import ( |
|
27 | build_data, api_call, assert_error, assert_ok, crash) | |
|
27 | build_data, api_call, assert_error, assert_ok, crash, jsonify) | |
|
28 | 28 | from rhodecode.tests.fixture import Fixture |
|
29 | 29 | |
|
30 | 30 | |
|
31 | 31 | fixture = Fixture() |
|
32 | 32 | |
|
33 | UPDATE_REPO_NAME = 'api_update_me' | |
|
34 | ||
|
35 | class SAME_AS_UPDATES(object): """ Constant used for tests below """ | |
|
33 | 36 | |
|
34 | 37 | @pytest.mark.usefixtures("testuser_api", "app") |
|
35 | 38 | class TestApiUpdateRepo(object): |
|
36 | @pytest.mark.parametrize("changing_attr, updates", [ | |
|
37 | ('owner', {'owner': TEST_USER_REGULAR_LOGIN}), | |
|
38 | ('description', {'description': 'new description'}), | |
|
39 | ('active', {'active': True}), | |
|
40 | ('active', {'active': False}), | |
|
41 |
('clone_uri', {'clone_uri': ' |
|
|
42 |
('clone_uri', {'clone_uri': |
|
|
43 |
( |
|
|
44 |
( |
|
|
45 |
( |
|
|
46 |
( |
|
|
47 |
( |
|
|
48 |
( |
|
|
39 | ||
|
40 | @pytest.mark.parametrize("updates, expected", [ | |
|
41 | ({'owner': TEST_USER_REGULAR_LOGIN}, SAME_AS_UPDATES), | |
|
42 | ({'description': 'new description'}, SAME_AS_UPDATES), | |
|
43 | ({'clone_uri': 'http://foo.com/repo'}, SAME_AS_UPDATES), | |
|
44 | ({'clone_uri': None}, {'clone_uri': ''}), | |
|
45 | ({'clone_uri': ''}, {'clone_uri': ''}), | |
|
46 | ({'landing_rev': 'branch:master'}, {'landing_rev': ['branch','master']}), | |
|
47 | ({'enable_statistics': True}, SAME_AS_UPDATES), | |
|
48 | ({'enable_locking': True}, SAME_AS_UPDATES), | |
|
49 | ({'enable_downloads': True}, SAME_AS_UPDATES), | |
|
50 | ({'name': 'new_repo_name'}, {'repo_name': 'new_repo_name'}), | |
|
51 | ({'group': 'test_group_for_update'}, | |
|
52 | {'repo_name': 'test_group_for_update/%s' % UPDATE_REPO_NAME}), | |
|
49 | 53 | ]) |
|
50 |
def test_api_update_repo(self, |
|
|
51 |
repo_name = |
|
|
54 | def test_api_update_repo(self, updates, expected, backend): | |
|
55 | repo_name = UPDATE_REPO_NAME | |
|
52 | 56 | repo = fixture.create_repo(repo_name, repo_type=backend.alias) |
|
53 |
if |
|
|
57 | if updates.get('group'): | |
|
54 | 58 | fixture.create_repo_group(updates['group']) |
|
55 | 59 | |
|
60 | expected_api_data = repo.get_api_data(include_secrets=True) | |
|
61 | if expected is SAME_AS_UPDATES: | |
|
62 | expected_api_data.update(updates) | |
|
63 | else: | |
|
64 | expected_api_data.update(expected) | |
|
65 | ||
|
66 | ||
|
56 | 67 | id_, params = build_data( |
|
57 | 68 | self.apikey, 'update_repo', repoid=repo_name, **updates) |
|
58 | 69 | response = api_call(self.app, params) |
|
59 | if changing_attr == 'name': | |
|
70 | ||
|
71 | if updates.get('name'): | |
|
60 | 72 | repo_name = updates['name'] |
|
61 |
if |
|
|
73 | if updates.get('group'): | |
|
62 | 74 | repo_name = '/'.join([updates['group'], repo_name]) |
|
75 | ||
|
63 | 76 | try: |
|
64 | 77 | expected = { |
|
65 | 78 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name), |
|
66 |
'repository': |
|
|
79 | 'repository': jsonify(expected_api_data) | |
|
67 | 80 | } |
|
68 | 81 | assert_ok(id_, expected, given=response.body) |
|
69 | 82 | finally: |
|
70 | 83 | fixture.destroy_repo(repo_name) |
|
71 |
if |
|
|
72 | ||
|
84 | if updates.get('group'): | |
|
73 | 85 | fixture.destroy_repo_group(updates['group']) |
|
74 | 86 | |
|
75 | 87 | def test_api_update_repo_fork_of_field(self, backend): |
|
76 | 88 | master_repo = backend.create_repo() |
|
77 | 89 | repo = backend.create_repo() |
|
78 | ||
|
79 | 90 | updates = { |
|
80 | 91 | 'fork_of': master_repo.repo_name |
|
81 | 92 | } |
|
93 | expected_api_data = repo.get_api_data(include_secrets=True) | |
|
94 | expected_api_data.update(updates) | |
|
95 | ||
|
82 | 96 | id_, params = build_data( |
|
83 | 97 | self.apikey, 'update_repo', repoid=repo.repo_name, **updates) |
|
84 | 98 | response = api_call(self.app, params) |
|
85 | 99 | expected = { |
|
86 | 100 | 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), |
|
87 |
'repository': |
|
|
101 | 'repository': jsonify(expected_api_data) | |
|
88 | 102 | } |
|
89 | 103 | assert_ok(id_, expected, given=response.body) |
|
90 | 104 | result = response.json['result']['repository'] |
@@ -131,7 +145,7 b' class TestApiUpdateRepo(object):' | |||
|
131 | 145 | |
|
132 | 146 | @mock.patch.object(RepoModel, 'update', crash) |
|
133 | 147 | def test_api_update_repo_exception_occurred(self, backend): |
|
134 |
repo_name = |
|
|
148 | repo_name = UPDATE_REPO_NAME | |
|
135 | 149 | fixture.create_repo(repo_name, repo_type=backend.alias) |
|
136 | 150 | id_, params = build_data( |
|
137 | 151 | self.apikey, 'update_repo', repoid=repo_name, |
@@ -25,7 +25,7 b' from rhodecode.model.user import UserMod' | |||
|
25 | 25 | from rhodecode.model.user_group import UserGroupModel |
|
26 | 26 | from rhodecode.tests import TEST_USER_REGULAR_LOGIN |
|
27 | 27 | from rhodecode.api.tests.utils import ( |
|
28 | build_data, api_call, assert_error, assert_ok, crash) | |
|
28 | build_data, api_call, assert_error, assert_ok, crash, jsonify) | |
|
29 | 29 | |
|
30 | 30 | |
|
31 | 31 | @pytest.mark.usefixtures("testuser_api", "app") |
@@ -40,14 +40,18 b' class TestUpdateUserGroup(object):' | |||
|
40 | 40 | def test_api_update_user_group(self, changing_attr, updates, user_util): |
|
41 | 41 | user_group = user_util.create_user_group() |
|
42 | 42 | group_name = user_group.users_group_name |
|
43 | expected_api_data = user_group.get_api_data() | |
|
44 | expected_api_data.update(updates) | |
|
45 | ||
|
43 | 46 | id_, params = build_data( |
|
44 | 47 | self.apikey, 'update_user_group', usergroupid=group_name, |
|
45 | 48 | **updates) |
|
46 | 49 | response = api_call(self.app, params) |
|
50 | ||
|
47 | 51 | expected = { |
|
48 | 52 | 'msg': 'updated user group ID:%s %s' % ( |
|
49 | 53 | user_group.users_group_id, user_group.users_group_name), |
|
50 |
'user_group': |
|
|
54 | 'user_group': jsonify(expected_api_data) | |
|
51 | 55 | } |
|
52 | 56 | assert_ok(id_, expected, given=response.body) |
|
53 | 57 | |
@@ -63,6 +67,10 b' class TestUpdateUserGroup(object):' | |||
|
63 | 67 | self, changing_attr, updates, user_util): |
|
64 | 68 | user_group = user_util.create_user_group() |
|
65 | 69 | group_name = user_group.users_group_name |
|
70 | expected_api_data = user_group.get_api_data() | |
|
71 | expected_api_data.update(updates) | |
|
72 | ||
|
73 | ||
|
66 | 74 | # grant permission to this user |
|
67 | 75 | user = UserModel().get_by_username(self.TEST_USER_LOGIN) |
|
68 | 76 | |
@@ -75,7 +83,7 b' class TestUpdateUserGroup(object):' | |||
|
75 | 83 | expected = { |
|
76 | 84 | 'msg': 'updated user group ID:%s %s' % ( |
|
77 | 85 | user_group.users_group_id, user_group.users_group_name), |
|
78 |
'user_group': |
|
|
86 | 'user_group': jsonify(expected_api_data) | |
|
79 | 87 | } |
|
80 | 88 | assert_ok(id_, expected, given=response.body) |
|
81 | 89 |
@@ -323,7 +323,7 b' def get_repo_changeset(request, apiuser,' | |||
|
323 | 323 | def get_repo_changesets(request, apiuser, repoid, start_rev, limit, |
|
324 | 324 | details=Optional('basic')): |
|
325 | 325 | """ |
|
326 |
Returns a set of c |
|
|
326 | Returns a set of commits limited by the number starting | |
|
327 | 327 | from the `start_rev` option. |
|
328 | 328 | |
|
329 | 329 | Additional parameters define the amount of details returned by this |
@@ -338,7 +338,7 b' def get_repo_changesets(request, apiuser' | |||
|
338 | 338 | :type repoid: str or int |
|
339 | 339 | :param start_rev: The starting revision from where to get changesets. |
|
340 | 340 | :type start_rev: str |
|
341 |
:param limit: Limit the number of c |
|
|
341 | :param limit: Limit the number of commits to this amount | |
|
342 | 342 | :type limit: str or int |
|
343 | 343 | :param details: Set the level of detail returned. Valid option are: |
|
344 | 344 | ``basic``, ``extended`` and ``full``. |
@@ -370,14 +370,17 b' def get_repo_changesets(request, apiuser' | |||
|
370 | 370 | |
|
371 | 371 | vcs_repo = repo.scm_instance() |
|
372 | 372 | # SVN needs a special case to distinguish its index and commit id |
|
373 | if vcs_repo.alias == 'svn' and (start_rev == '0'): | |
|
373 | if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'): | |
|
374 | 374 | start_rev = vcs_repo.commit_ids[0] |
|
375 | 375 | |
|
376 | 376 | try: |
|
377 |
commits = repo |
|
|
377 | commits = vcs_repo.get_commits( | |
|
378 | 378 | start_id=start_rev, pre_load=pre_load) |
|
379 | 379 | except TypeError as e: |
|
380 | 380 | raise JSONRPCError(e.message) |
|
381 | except Exception: | |
|
382 | log.exception('Fetching of commits failed') | |
|
383 | raise JSONRPCError('Error occurred during commit fetching') | |
|
381 | 384 | |
|
382 | 385 | ret = [] |
|
383 | 386 | for cnt, commit in enumerate(commits): |
@@ -19,6 +19,7 b'' | |||
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | import importlib | |
|
22 | 23 | |
|
23 | 24 | from pkg_resources import iter_entry_points |
|
24 | 25 | from pyramid.authentication import SessionAuthenticationPolicy |
@@ -27,9 +28,15 b' from rhodecode.authentication.registry i' | |||
|
27 | 28 | from rhodecode.authentication.routes import root_factory |
|
28 | 29 | from rhodecode.authentication.routes import AuthnRootResource |
|
29 | 30 | from rhodecode.config.routing import ADMIN_PREFIX |
|
31 | from rhodecode.model.settings import SettingsModel | |
|
32 | ||
|
30 | 33 | |
|
31 | 34 | log = logging.getLogger(__name__) |
|
32 | 35 | |
|
36 | # Plugin ID prefixes to distinct between normal and legacy plugins. | |
|
37 | plugin_prefix = 'egg:' | |
|
38 | legacy_plugin_prefix = 'py:' | |
|
39 | ||
|
33 | 40 | |
|
34 | 41 | # TODO: Currently this is only used to discover the authentication plugins. |
|
35 | 42 | # Later on this may be used in a generic way to look up and include all kinds |
@@ -38,16 +45,45 b' log = logging.getLogger(__name__)' | |||
|
38 | 45 | # TODO: When refactoring this think about splitting it up into distinct |
|
39 | 46 | # discover, load and include phases. |
|
40 | 47 | def _discover_plugins(config, entry_point='enterprise.plugins1'): |
|
41 | _discovered_plugins = {} | |
|
42 | ||
|
43 | 48 | for ep in iter_entry_points(entry_point): |
|
44 |
plugin_id = ' |
|
|
49 | plugin_id = '{}{}#{}'.format( | |
|
50 | plugin_prefix, ep.dist.project_name, ep.name) | |
|
45 | 51 | log.debug('Plugin discovered: "%s"', plugin_id) |
|
46 | module = ep.load() | |
|
47 | plugin = module(plugin_id=plugin_id) | |
|
48 | config.include(plugin.includeme) | |
|
52 | try: | |
|
53 | module = ep.load() | |
|
54 | plugin = module(plugin_id=plugin_id) | |
|
55 | config.include(plugin.includeme) | |
|
56 | except Exception as e: | |
|
57 | log.exception( | |
|
58 | 'Exception while loading authentication plugin ' | |
|
59 | '"{}": {}'.format(plugin_id, e.message)) | |
|
60 | ||
|
61 | ||
|
62 | def _import_legacy_plugin(plugin_id): | |
|
63 | module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1] | |
|
64 | module = importlib.import_module(module_name) | |
|
65 | return module.plugin_factory(plugin_id=plugin_id) | |
|
66 | ||
|
49 | 67 | |
|
50 | return _discovered_plugins | |
|
68 | def _discover_legacy_plugins(config, prefix=legacy_plugin_prefix): | |
|
69 | """ | |
|
70 | Function that imports the legacy plugins stored in the 'auth_plugins' | |
|
71 | setting in database which are using the specified prefix. Normally 'py:' is | |
|
72 | used for the legacy plugins. | |
|
73 | """ | |
|
74 | auth_plugins = SettingsModel().get_setting_by_name('auth_plugins') | |
|
75 | enabled_plugins = auth_plugins.app_settings_value | |
|
76 | legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)] | |
|
77 | ||
|
78 | for plugin_id in legacy_plugins: | |
|
79 | log.debug('Legacy plugin discovered: "%s"', plugin_id) | |
|
80 | try: | |
|
81 | plugin = _import_legacy_plugin(plugin_id) | |
|
82 | config.include(plugin.includeme) | |
|
83 | except Exception as e: | |
|
84 | log.exception( | |
|
85 | 'Exception while loading legacy authentication plugin ' | |
|
86 | '"{}": {}'.format(plugin_id, e.message)) | |
|
51 | 87 | |
|
52 | 88 | |
|
53 | 89 | def includeme(config): |
@@ -56,7 +92,7 b' def includeme(config):' | |||
|
56 | 92 | config.set_authentication_policy(authn_policy) |
|
57 | 93 | |
|
58 | 94 | # Create authentication plugin registry and add it to the pyramid registry. |
|
59 | authn_registry = AuthenticationPluginRegistry() | |
|
95 | authn_registry = AuthenticationPluginRegistry(config.get_settings()) | |
|
60 | 96 | config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin) |
|
61 | 97 | config.registry.registerUtility(authn_registry) |
|
62 | 98 | |
@@ -83,3 +119,4 b' def includeme(config):' | |||
|
83 | 119 | |
|
84 | 120 | # Auto discover authentication plugins and include their configuration. |
|
85 | 121 | _discover_plugins(config) |
|
122 | _discover_legacy_plugins(config) |
@@ -25,24 +25,18 b' Authentication modules' | |||
|
25 | 25 | import logging |
|
26 | 26 | import time |
|
27 | 27 | import traceback |
|
28 | import warnings | |
|
28 | 29 | |
|
29 | from authomatic import Authomatic | |
|
30 | from authomatic.adapters import WebObAdapter | |
|
31 | from authomatic.providers import oauth2, oauth1 | |
|
32 | from pylons import url | |
|
33 | from pylons.controllers.util import Response | |
|
34 | from pylons.i18n.translation import _ | |
|
35 | 30 | from pyramid.threadlocal import get_current_registry |
|
36 | 31 | from sqlalchemy.ext.hybrid import hybrid_property |
|
37 | 32 | |
|
38 | import rhodecode.lib.helpers as h | |
|
39 | 33 | from rhodecode.authentication.interface import IAuthnPluginRegistry |
|
40 | 34 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
41 | 35 | from rhodecode.lib import caches |
|
42 | 36 | from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt |
|
43 | 37 | from rhodecode.lib.utils2 import md5_safe, safe_int |
|
44 | 38 | from rhodecode.lib.utils2 import safe_str |
|
45 |
from rhodecode.model.db import User |
|
|
39 | from rhodecode.model.db import User | |
|
46 | 40 | from rhodecode.model.meta import Session |
|
47 | 41 | from rhodecode.model.settings import SettingsModel |
|
48 | 42 | from rhodecode.model.user import UserModel |
@@ -226,17 +220,23 b' class RhodeCodeAuthPluginBase(object):' | |||
|
226 | 220 | """ |
|
227 | 221 | raise NotImplementedError("Not implemented in base class") |
|
228 | 222 | |
|
223 | @property | |
|
224 | def is_headers_auth(self): | |
|
225 | """ | |
|
226 | Returns True if this authentication plugin uses HTTP headers as | |
|
227 | authentication method. | |
|
228 | """ | |
|
229 | return False | |
|
230 | ||
|
229 | 231 | @hybrid_property |
|
230 | 232 | def is_container_auth(self): |
|
231 | 233 | """ |
|
232 | Returns bool if this module uses container auth. | |
|
233 | ||
|
234 | This property will trigger an automatic call to authenticate on | |
|
235 | a visit to the website or during a push/pull. | |
|
236 | ||
|
237 | :returns: bool | |
|
234 | Deprecated method that indicates if this authentication plugin uses | |
|
235 | HTTP headers as authentication method. | |
|
238 | 236 | """ |
|
239 | return False | |
|
237 | warnings.warn( | |
|
238 | 'Use is_headers_auth instead.', category=DeprecationWarning) | |
|
239 | return self.is_headers_auth | |
|
240 | 240 | |
|
241 | 241 | @hybrid_property |
|
242 | 242 | def allows_creating_users(self): |
@@ -299,7 +299,7 b' class RhodeCodeAuthPluginBase(object):' | |||
|
299 | 299 | """ |
|
300 | 300 | Helper method for user fetching in plugins, by default it's using |
|
301 | 301 | simple fetch by username, but this method can be custimized in plugins |
|
302 |
eg. |
|
|
302 | eg. headers auth plugin to fetch user by environ params | |
|
303 | 303 | |
|
304 | 304 | :param username: username if given to fetch from database |
|
305 | 305 | :param kwargs: extra arguments needed for user fetching. |
@@ -477,131 +477,11 b' class RhodeCodeExternalAuthPlugin(RhodeC' | |||
|
477 | 477 | return auth |
|
478 | 478 | |
|
479 | 479 | |
|
480 | class AuthomaticBase(RhodeCodeExternalAuthPlugin): | |
|
481 | ||
|
482 | # TODO: Think about how to create and store this secret string. | |
|
483 | # We need the secret for the authomatic library. It needs to be the same | |
|
484 | # across requests. | |
|
485 | def _get_authomatic_secret(self, length=40): | |
|
486 | secret = self.get_setting_by_name('secret') | |
|
487 | if secret is None or secret == 'None' or secret == '': | |
|
488 | from Crypto import Random, Hash | |
|
489 | secret_bytes = Random.new().read(length) | |
|
490 | secret_hash = Hash.SHA256.new() | |
|
491 | secret_hash.update(secret_bytes) | |
|
492 | secret = secret_hash.hexdigest() | |
|
493 | self.create_or_update_setting('secret', secret) | |
|
494 | Session.commit() | |
|
495 | secret = self.get_setting_by_name('secret') | |
|
496 | return secret | |
|
497 | ||
|
498 | def get_authomatic(self): | |
|
499 | scope = [] | |
|
500 | if self.name == 'bitbucket': | |
|
501 | provider_class = oauth1.Bitbucket | |
|
502 | scope = ['account', 'email', 'repository', 'issue', 'issue:write'] | |
|
503 | elif self.name == 'github': | |
|
504 | provider_class = oauth2.GitHub | |
|
505 | scope = ['repo', 'public_repo', 'user:email'] | |
|
506 | elif self.name == 'google': | |
|
507 | provider_class = oauth2.Google | |
|
508 | scope = ['profile', 'email'] | |
|
509 | elif self.name == 'twitter': | |
|
510 | provider_class = oauth1.Twitter | |
|
511 | ||
|
512 | authomatic_conf = { | |
|
513 | self.name: { | |
|
514 | 'class_': provider_class, | |
|
515 | 'consumer_key': self.get_setting_by_name('consumer_key'), | |
|
516 | 'consumer_secret': self.get_setting_by_name('consumer_secret'), | |
|
517 | 'scope': scope, | |
|
518 | 'access_headers': {'User-Agent': 'TestAppAgent'}, | |
|
519 | } | |
|
520 | } | |
|
521 | secret = self._get_authomatic_secret() | |
|
522 | return Authomatic(config=authomatic_conf, | |
|
523 | secret=secret) | |
|
524 | ||
|
525 | def get_provider_result(self, request): | |
|
526 | """ | |
|
527 | Provides `authomatic.core.LoginResult` for provider and request | |
|
528 | ||
|
529 | :param provider_name: | |
|
530 | :param request: | |
|
531 | :param config: | |
|
532 | :return: | |
|
533 | """ | |
|
534 | response = Response() | |
|
535 | adapter = WebObAdapter(request, response) | |
|
536 | authomatic_inst = self.get_authomatic() | |
|
537 | return authomatic_inst.login(adapter, self.name), response | |
|
538 | ||
|
539 | def handle_social_data(self, session, user_id, social_data): | |
|
540 | """ | |
|
541 | Updates user tokens in database whenever necessary | |
|
542 | :param request: | |
|
543 | :param user: | |
|
544 | :param social_data: | |
|
545 | :return: | |
|
546 | """ | |
|
547 | if not self.is_active(): | |
|
548 | h.flash(_('This provider is currently disabled'), | |
|
549 | category='warning') | |
|
550 | return False | |
|
551 | ||
|
552 | social_data = social_data | |
|
553 | update_identity = False | |
|
554 | ||
|
555 | existing_row = ExternalIdentity.by_external_id_and_provider( | |
|
556 | social_data['user']['id'], | |
|
557 | social_data['credentials.provider'] | |
|
558 | ) | |
|
559 | ||
|
560 | if existing_row: | |
|
561 | Session().delete(existing_row) | |
|
562 | update_identity = True | |
|
563 | ||
|
564 | if not existing_row or update_identity: | |
|
565 | if not update_identity: | |
|
566 | h.flash(_('Your external identity is now ' | |
|
567 | 'connected with your account'), category='success') | |
|
568 | ||
|
569 | if not social_data['user']['id']: | |
|
570 | h.flash(_('No external user id found? Perhaps permissions' | |
|
571 | 'for authentication are set incorrectly'), | |
|
572 | category='error') | |
|
573 | return False | |
|
574 | ||
|
575 | ex_identity = ExternalIdentity() | |
|
576 | ex_identity.external_id = social_data['user']['id'] | |
|
577 | ex_identity.external_username = social_data['user']['user_name'] | |
|
578 | ex_identity.provider_name = social_data['credentials.provider'] | |
|
579 | ex_identity.access_token = social_data['credentials.token'] | |
|
580 | ex_identity.token_secret = social_data['credentials.token_secret'] | |
|
581 | ex_identity.alt_token = social_data['credentials.refresh_token'] | |
|
582 | ex_identity.local_user_id = user_id | |
|
583 | Session().add(ex_identity) | |
|
584 | session.pop('rhodecode.social_auth', None) | |
|
585 | return ex_identity | |
|
586 | ||
|
587 | def callback_url(self): | |
|
588 | try: | |
|
589 | return url('social_auth', provider_name=self.name, qualified=True) | |
|
590 | except TypeError: | |
|
591 | pass | |
|
592 | return '' | |
|
593 | ||
|
594 | ||
|
595 | 480 | def loadplugin(plugin_id): |
|
596 | 481 | """ |
|
597 | 482 | Loads and returns an instantiated authentication plugin. |
|
598 | 483 | Returns the RhodeCodeAuthPluginBase subclass on success, |
|
599 |
|
|
|
600 | ||
|
601 | raises: | |
|
602 | KeyError -- if no plugin available with given name | |
|
603 | TypeError -- if the RhodeCodeAuthPlugin is not a subclass of | |
|
604 | ours RhodeCodeAuthPluginBase | |
|
484 | or None on failure. | |
|
605 | 485 | """ |
|
606 | 486 | # TODO: Disusing pyramids thread locals to retrieve the registry. |
|
607 | 487 | authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) |
@@ -622,9 +502,9 b' def authenticate(username, password, env' | |||
|
622 | 502 | Authentication function used for access control, |
|
623 | 503 | It tries to authenticate based on enabled authentication modules. |
|
624 | 504 | |
|
625 |
:param username: username can be empty for |
|
|
626 |
:param password: password can be empty for |
|
|
627 |
:param environ: environ headers passed for |
|
|
505 | :param username: username can be empty for headers auth | |
|
506 | :param password: password can be empty for headers auth | |
|
507 | :param environ: environ headers passed for headers auth | |
|
628 | 508 | :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE` |
|
629 | 509 | :param skip_missing: ignores plugins that are in db but not in environment |
|
630 | 510 | :returns: None if auth failed, plugin_user dict if auth is correct |
@@ -632,51 +512,41 b' def authenticate(username, password, env' | |||
|
632 | 512 | if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]: |
|
633 | 513 | raise ValueError('auth type must be on of http, vcs got "%s" instead' |
|
634 | 514 | % auth_type) |
|
635 |
|
|
|
636 | auth_plugins = SettingsModel().get_auth_plugins() | |
|
637 | for plugin_id in auth_plugins: | |
|
638 | plugin = loadplugin(plugin_id) | |
|
515 | headers_only = environ and not (username and password) | |
|
639 | 516 | |
|
640 | if plugin is None: | |
|
641 | log.warning('Authentication plugin missing: "{}"'.format( | |
|
642 | plugin_id)) | |
|
643 | continue | |
|
644 | ||
|
645 | if not plugin.is_active(): | |
|
646 | log.info('Authentication plugin is inactive: "{}"'.format( | |
|
647 | plugin_id)) | |
|
648 | continue | |
|
649 | ||
|
517 | authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) | |
|
518 | for plugin in authn_registry.get_plugins_for_authentication(): | |
|
650 | 519 | plugin.set_auth_type(auth_type) |
|
651 | 520 | user = plugin.get_user(username) |
|
652 | 521 | display_user = user.username if user else username |
|
653 | 522 | |
|
654 |
if |
|
|
655 |
log.debug('Auth type is for |
|
|
656 |
' |
|
|
523 | if headers_only and not plugin.is_headers_auth: | |
|
524 | log.debug('Auth type is for headers only and plugin `%s` is not ' | |
|
525 | 'headers plugin, skipping...', plugin.get_id()) | |
|
657 | 526 | continue |
|
658 | 527 | |
|
659 | 528 | # load plugin settings from RhodeCode database |
|
660 | 529 | plugin_settings = plugin.get_settings() |
|
661 | 530 | log.debug('Plugin settings:%s', plugin_settings) |
|
662 | 531 | |
|
663 | log.debug('Trying authentication using ** %s **', plugin_id) | |
|
532 | log.debug('Trying authentication using ** %s **', plugin.get_id()) | |
|
664 | 533 | # use plugin's method of user extraction. |
|
665 | 534 | user = plugin.get_user(username, environ=environ, |
|
666 | 535 | settings=plugin_settings) |
|
667 | 536 | display_user = user.username if user else username |
|
668 | log.debug('Plugin %s extracted user is `%s`', plugin_id, display_user) | |
|
537 | log.debug( | |
|
538 | 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user) | |
|
669 | 539 | |
|
670 | 540 | if not plugin.allows_authentication_from(user): |
|
671 | 541 | log.debug('Plugin %s does not accept user `%s` for authentication', |
|
672 | plugin_id, display_user) | |
|
542 | plugin.get_id(), display_user) | |
|
673 | 543 | continue |
|
674 | 544 | else: |
|
675 | 545 | log.debug('Plugin %s accepted user `%s` for authentication', |
|
676 | plugin_id, display_user) | |
|
546 | plugin.get_id(), display_user) | |
|
677 | 547 | |
|
678 | 548 | log.info('Authenticating user `%s` using %s plugin', |
|
679 | display_user, plugin_id) | |
|
549 | display_user, plugin.get_id()) | |
|
680 | 550 | |
|
681 | 551 | _cache_ttl = 0 |
|
682 | 552 | |
@@ -691,7 +561,7 b' def authenticate(username, password, env' | |||
|
691 | 561 | # get instance of cache manager configured for a namespace |
|
692 | 562 | cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl) |
|
693 | 563 | |
|
694 | log.debug('Cache for plugin `%s` active: %s', plugin_id, | |
|
564 | log.debug('Cache for plugin `%s` active: %s', plugin.get_id(), | |
|
695 | 565 | plugin_cache_active) |
|
696 | 566 | |
|
697 | 567 | # for environ based password can be empty, but then the validation is |
@@ -706,7 +576,7 b' def authenticate(username, password, env' | |||
|
706 | 576 | # then auth is correct. |
|
707 | 577 | start = time.time() |
|
708 | 578 | log.debug('Running plugin `%s` _authenticate method', |
|
709 | plugin_id) | |
|
579 | plugin.get_id()) | |
|
710 | 580 | |
|
711 | 581 | def auth_func(): |
|
712 | 582 | """ |
@@ -726,7 +596,7 b' def authenticate(username, password, env' | |||
|
726 | 596 | auth_time = time.time() - start |
|
727 | 597 | log.debug('Authentication for plugin `%s` completed in %.3fs, ' |
|
728 | 598 | 'expiration time of fetched cache %.1fs.', |
|
729 | plugin_id, auth_time, _cache_ttl) | |
|
599 | plugin.get_id(), auth_time, _cache_ttl) | |
|
730 | 600 | |
|
731 | 601 | log.debug('PLUGIN USER DATA: %s', plugin_user) |
|
732 | 602 | |
@@ -735,5 +605,5 b' def authenticate(username, password, env' | |||
|
735 | 605 | return plugin_user |
|
736 | 606 | # we failed to Auth because .auth() method didn't return proper user |
|
737 | 607 | log.debug("User `%s` failed to authenticate against %s", |
|
738 | display_user, plugin_id) | |
|
608 | display_user, plugin.get_id()) | |
|
739 | 609 | return None |
@@ -34,6 +34,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||
|
34 | 34 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
35 | 35 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
36 | 36 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
37 | from rhodecode.lib.colander_utils import strip_whitespace | |
|
37 | 38 | from rhodecode.lib.ext_json import json, formatted_json |
|
38 | 39 | from rhodecode.model.db import User |
|
39 | 40 | |
@@ -58,12 +59,14 b' class CrowdSettingsSchema(AuthnPluginSet' | |||
|
58 | 59 | colander.String(), |
|
59 | 60 | default='127.0.0.1', |
|
60 | 61 | description=_('The FQDN or IP of the Atlassian CROWD Server'), |
|
62 | preparer=strip_whitespace, | |
|
61 | 63 | title=_('Host'), |
|
62 | 64 | widget='string') |
|
63 | 65 | port = colander.SchemaNode( |
|
64 | 66 | colander.Int(), |
|
65 | 67 | default=8095, |
|
66 | 68 | description=_('The Port in use by the Atlassian CROWD Server'), |
|
69 | preparer=strip_whitespace, | |
|
67 | 70 | title=_('Port'), |
|
68 | 71 | validator=colander.Range(min=0, max=65536), |
|
69 | 72 | widget='int') |
@@ -71,12 +74,14 b' class CrowdSettingsSchema(AuthnPluginSet' | |||
|
71 | 74 | colander.String(), |
|
72 | 75 | default='', |
|
73 | 76 | description=_('The Application Name to authenticate to CROWD'), |
|
77 | preparer=strip_whitespace, | |
|
74 | 78 | title=_('Application Name'), |
|
75 | 79 | widget='string') |
|
76 | 80 | app_password = colander.SchemaNode( |
|
77 | 81 | colander.String(), |
|
78 | 82 | default='', |
|
79 | 83 | description=_('The password to authenticate to CROWD'), |
|
84 | preparer=strip_whitespace, | |
|
80 | 85 | title=_('Application Password'), |
|
81 | 86 | widget='password') |
|
82 | 87 | admin_groups = colander.SchemaNode( |
@@ -85,6 +90,7 b' class CrowdSettingsSchema(AuthnPluginSet' | |||
|
85 | 90 | description=_('A comma separated list of group names that identify ' |
|
86 | 91 | 'users as RhodeCode Administrators'), |
|
87 | 92 | missing='', |
|
93 | preparer=strip_whitespace, | |
|
88 | 94 | title=_('Admin Groups'), |
|
89 | 95 | widget='string') |
|
90 | 96 | |
@@ -191,12 +197,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||
|
191 | 197 | config.add_view( |
|
192 | 198 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
193 | 199 | attr='settings_get', |
|
200 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
194 | 201 | request_method='GET', |
|
195 | 202 | route_name='auth_home', |
|
196 | 203 | context=CrowdAuthnResource) |
|
197 | 204 | config.add_view( |
|
198 | 205 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
199 | 206 | attr='settings_post', |
|
207 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
200 | 208 | request_method='POST', |
|
201 | 209 | route_name='auth_home', |
|
202 | 210 | context=CrowdAuthnResource) |
@@ -36,6 +36,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||
|
36 | 36 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
37 | 37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
38 | 38 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
39 | from rhodecode.lib.colander_utils import strip_whitespace | |
|
39 | 40 | from rhodecode.lib.utils2 import safe_unicode |
|
40 | 41 | from rhodecode.model.db import User |
|
41 | 42 | |
@@ -60,6 +61,7 b' class JasigCasSettingsSchema(AuthnPlugin' | |||
|
60 | 61 | colander.String(), |
|
61 | 62 | default='https://domain.com/cas/v1/tickets', |
|
62 | 63 | description=_('The url of the Jasig CAS REST service'), |
|
64 | preparer=strip_whitespace, | |
|
63 | 65 | title=_('URL'), |
|
64 | 66 | widget='string') |
|
65 | 67 | |
@@ -72,12 +74,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||
|
72 | 74 | config.add_view( |
|
73 | 75 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
74 | 76 | attr='settings_get', |
|
77 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
75 | 78 | request_method='GET', |
|
76 | 79 | route_name='auth_home', |
|
77 | 80 | context=JasigCasAuthnResource) |
|
78 | 81 | config.add_view( |
|
79 | 82 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
80 | 83 | attr='settings_post', |
|
84 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
81 | 85 | request_method='POST', |
|
82 | 86 | route_name='auth_home', |
|
83 | 87 | context=JasigCasAuthnResource) |
@@ -92,8 +96,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||
|
92 | 96 | def name(self): |
|
93 | 97 | return "jasig-cas" |
|
94 | 98 | |
|
95 |
@ |
|
|
96 |
def is_ |
|
|
99 | @property | |
|
100 | def is_headers_auth(self): | |
|
97 | 101 | return True |
|
98 | 102 | |
|
99 | 103 | def use_fake_password(self): |
@@ -33,6 +33,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||
|
33 | 33 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
34 | 34 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
35 | 35 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
36 | from rhodecode.lib.colander_utils import strip_whitespace | |
|
36 | 37 | from rhodecode.lib.exceptions import ( |
|
37 | 38 | LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError |
|
38 | 39 | ) |
@@ -45,8 +46,9 b' log = logging.getLogger(__name__)' | |||
|
45 | 46 | try: |
|
46 | 47 | import ldap |
|
47 | 48 | except ImportError: |
|
48 | # means that python-ldap is not installed | |
|
49 |
|
|
|
49 | # means that python-ldap is not installed, we use Missing object to mark | |
|
50 | # ldap lib is Missing | |
|
51 | ldap = Missing | |
|
50 | 52 | |
|
51 | 53 | |
|
52 | 54 | def plugin_factory(plugin_id, *args, **kwds): |
@@ -71,12 +73,14 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
71 | 73 | colander.String(), |
|
72 | 74 | default='', |
|
73 | 75 | description=_('Host of the LDAP Server'), |
|
76 | preparer=strip_whitespace, | |
|
74 | 77 | title=_('LDAP Host'), |
|
75 | 78 | widget='string') |
|
76 | 79 | port = colander.SchemaNode( |
|
77 | 80 | colander.Int(), |
|
78 | 81 | default=389, |
|
79 | 82 | description=_('Port that the LDAP server is listening on'), |
|
83 | preparer=strip_whitespace, | |
|
80 | 84 | title=_('Port'), |
|
81 | 85 | validator=colander.Range(min=0, max=65536), |
|
82 | 86 | widget='int') |
@@ -85,6 +89,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
85 | 89 | default='', |
|
86 | 90 | description=_('User to connect to LDAP'), |
|
87 | 91 | missing='', |
|
92 | preparer=strip_whitespace, | |
|
88 | 93 | title=_('Account'), |
|
89 | 94 | widget='string') |
|
90 | 95 | dn_pass = colander.SchemaNode( |
@@ -92,6 +97,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
92 | 97 | default='', |
|
93 | 98 | description=_('Password to connect to LDAP'), |
|
94 | 99 | missing='', |
|
100 | preparer=strip_whitespace, | |
|
95 | 101 | title=_('Password'), |
|
96 | 102 | widget='password') |
|
97 | 103 | tls_kind = colander.SchemaNode( |
@@ -113,6 +119,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
113 | 119 | default='', |
|
114 | 120 | description=_('Base DN to search (e.g., dc=mydomain,dc=com)'), |
|
115 | 121 | missing='', |
|
122 | preparer=strip_whitespace, | |
|
116 | 123 | title=_('Base DN'), |
|
117 | 124 | widget='string') |
|
118 | 125 | filter = colander.SchemaNode( |
@@ -120,6 +127,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
120 | 127 | default='', |
|
121 | 128 | description=_('Filter to narrow results (e.g., ou=Users, etc)'), |
|
122 | 129 | missing='', |
|
130 | preparer=strip_whitespace, | |
|
123 | 131 | title=_('LDAP Search Filter'), |
|
124 | 132 | widget='string') |
|
125 | 133 | search_scope = colander.SchemaNode( |
@@ -133,14 +141,16 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
133 | 141 | colander.String(), |
|
134 | 142 | default='', |
|
135 | 143 | description=_('LDAP Attribute to map to user name'), |
|
144 | missing_msg=_('The LDAP Login attribute of the CN must be specified'), | |
|
145 | preparer=strip_whitespace, | |
|
136 | 146 | title=_('Login Attribute'), |
|
137 | missing_msg=_('The LDAP Login attribute of the CN must be specified'), | |
|
138 | 147 | widget='string') |
|
139 | 148 | attr_firstname = colander.SchemaNode( |
|
140 | 149 | colander.String(), |
|
141 | 150 | default='', |
|
142 | 151 | description=_('LDAP Attribute to map to first name'), |
|
143 | 152 | missing='', |
|
153 | preparer=strip_whitespace, | |
|
144 | 154 | title=_('First Name Attribute'), |
|
145 | 155 | widget='string') |
|
146 | 156 | attr_lastname = colander.SchemaNode( |
@@ -148,6 +158,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
148 | 158 | default='', |
|
149 | 159 | description=_('LDAP Attribute to map to last name'), |
|
150 | 160 | missing='', |
|
161 | preparer=strip_whitespace, | |
|
151 | 162 | title=_('Last Name Attribute'), |
|
152 | 163 | widget='string') |
|
153 | 164 | attr_email = colander.SchemaNode( |
@@ -155,6 +166,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||
|
155 | 166 | default='', |
|
156 | 167 | description=_('LDAP Attribute to map to email address'), |
|
157 | 168 | missing='', |
|
169 | preparer=strip_whitespace, | |
|
158 | 170 | title=_('Email Attribute'), |
|
159 | 171 | widget='string') |
|
160 | 172 | |
@@ -171,7 +183,7 b' class AuthLdap(object):' | |||
|
171 | 183 | tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, |
|
172 | 184 | search_scope='SUBTREE', attr_login='uid', |
|
173 | 185 | ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'): |
|
174 |
if |
|
|
186 | if ldap == Missing: | |
|
175 | 187 | raise LdapImportError("Missing or incompatible ldap library") |
|
176 | 188 | |
|
177 | 189 | self.ldap_version = ldap_version |
@@ -317,12 +329,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||
|
317 | 329 | config.add_view( |
|
318 | 330 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
319 | 331 | attr='settings_get', |
|
332 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
320 | 333 | request_method='GET', |
|
321 | 334 | route_name='auth_home', |
|
322 | 335 | context=LdapAuthnResource) |
|
323 | 336 | config.add_view( |
|
324 | 337 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
325 | 338 | attr='settings_post', |
|
339 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
326 | 340 | request_method='POST', |
|
327 | 341 | route_name='auth_home', |
|
328 | 342 | context=LdapAuthnResource) |
@@ -35,6 +35,7 b' from sqlalchemy.ext.hybrid import hybrid' | |||
|
35 | 35 | from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin |
|
36 | 36 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
37 | 37 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
38 | from rhodecode.lib.colander_utils import strip_whitespace | |
|
38 | 39 | |
|
39 | 40 | log = logging.getLogger(__name__) |
|
40 | 41 | |
@@ -57,6 +58,7 b' class PamSettingsSchema(AuthnPluginSetti' | |||
|
57 | 58 | colander.String(), |
|
58 | 59 | default='login', |
|
59 | 60 | description=_('PAM service name to use for authentication.'), |
|
61 | preparer=strip_whitespace, | |
|
60 | 62 | title=_('PAM service name'), |
|
61 | 63 | widget='string') |
|
62 | 64 | gecos = colander.SchemaNode( |
@@ -64,6 +66,7 b' class PamSettingsSchema(AuthnPluginSetti' | |||
|
64 | 66 | default='(?P<last_name>.+),\s*(?P<first_name>\w+)', |
|
65 | 67 | description=_('Regular expression for extracting user name/email etc. ' |
|
66 | 68 | 'from Unix userinfo.'), |
|
69 | preparer=strip_whitespace, | |
|
67 | 70 | title=_('Gecos Regex'), |
|
68 | 71 | widget='string') |
|
69 | 72 | |
@@ -79,12 +82,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter' | |||
|
79 | 82 | config.add_view( |
|
80 | 83 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
81 | 84 | attr='settings_get', |
|
85 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
82 | 86 | request_method='GET', |
|
83 | 87 | route_name='auth_home', |
|
84 | 88 | context=PamAuthnResource) |
|
85 | 89 | config.add_view( |
|
86 | 90 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
87 | 91 | attr='settings_post', |
|
92 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
88 | 93 | request_method='POST', |
|
89 | 94 | route_name='auth_home', |
|
90 | 95 | context=PamAuthnResource) |
@@ -52,12 +52,14 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP' | |||
|
52 | 52 | config.add_view( |
|
53 | 53 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
54 | 54 | attr='settings_get', |
|
55 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
55 | 56 | request_method='GET', |
|
56 | 57 | route_name='auth_home', |
|
57 | 58 | context=RhodecodeAuthnResource) |
|
58 | 59 | config.add_view( |
|
59 | 60 | 'rhodecode.authentication.views.AuthnPluginViewBase', |
|
60 | 61 | attr='settings_post', |
|
62 | renderer='rhodecode:templates/admin/auth/plugin_settings.html', | |
|
61 | 63 | request_method='POST', |
|
62 | 64 | route_name='auth_home', |
|
63 | 65 | context=RhodecodeAuthnResource) |
@@ -25,14 +25,20 b' from zope.interface import implementer' | |||
|
25 | 25 | |
|
26 | 26 | from rhodecode.authentication.interface import IAuthnPluginRegistry |
|
27 | 27 | from rhodecode.lib.utils2 import safe_str |
|
28 | from rhodecode.model.settings import SettingsModel | |
|
28 | 29 | |
|
29 | 30 | log = logging.getLogger(__name__) |
|
30 | 31 | |
|
31 | 32 | |
|
32 | 33 | @implementer(IAuthnPluginRegistry) |
|
33 | 34 | class AuthenticationPluginRegistry(object): |
|
34 | def __init__(self): | |
|
35 | ||
|
36 | # INI settings key to set a fallback authentication plugin. | |
|
37 | fallback_plugin_key = 'rhodecode.auth_plugin_fallback' | |
|
38 | ||
|
39 | def __init__(self, settings): | |
|
35 | 40 | self._plugins = {} |
|
41 | self._fallback_plugin = settings.get(self.fallback_plugin_key, None) | |
|
36 | 42 | |
|
37 | 43 | def add_authn_plugin(self, config, plugin): |
|
38 | 44 | plugin_id = plugin.get_id() |
@@ -51,3 +57,31 b' class AuthenticationPluginRegistry(objec' | |||
|
51 | 57 | |
|
52 | 58 | def get_plugin(self, plugin_id): |
|
53 | 59 | return self._plugins.get(plugin_id, None) |
|
60 | ||
|
61 | def get_plugins_for_authentication(self): | |
|
62 | """ | |
|
63 | Returns a list of plugins which should be consulted when authenticating | |
|
64 | a user. It only returns plugins which are enabled and active. | |
|
65 | Additionally it includes the fallback plugin from the INI file, if | |
|
66 | `rhodecode.auth_plugin_fallback` is set to a plugin ID. | |
|
67 | """ | |
|
68 | plugins = [] | |
|
69 | ||
|
70 | # Add all enabled and active plugins to the list. We iterate over the | |
|
71 | # auth_plugins setting from DB beacuse it also represents the ordering. | |
|
72 | enabled_plugins = SettingsModel().get_auth_plugins() | |
|
73 | for plugin_id in enabled_plugins: | |
|
74 | plugin = self.get_plugin(plugin_id) | |
|
75 | if plugin is not None and plugin.is_active(): | |
|
76 | plugins.append(plugin) | |
|
77 | ||
|
78 | # Add the fallback plugin from ini file. | |
|
79 | if self._fallback_plugin: | |
|
80 | log.warn( | |
|
81 | 'Using fallback authentication plugin from INI file: "%s"', | |
|
82 | self._fallback_plugin) | |
|
83 | plugin = self.get_plugin(self._fallback_plugin) | |
|
84 | if plugin is not None and plugin not in plugins: | |
|
85 | plugins.append(plugin) | |
|
86 | ||
|
87 | return plugins |
@@ -21,12 +21,11 b'' | |||
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.exceptions import ConfigurationError |
|
24 | from pyramid.i18n import TranslationStringFactory | |
|
25 | 24 | |
|
26 | 25 | from rhodecode.lib.utils2 import safe_str |
|
27 | 26 | from rhodecode.model.settings import SettingsModel |
|
27 | from rhodecode.translation import _ | |
|
28 | 28 | |
|
29 | _ = TranslationStringFactory('rhodecode-enterprise') | |
|
30 | 29 | |
|
31 | 30 | log = logging.getLogger(__name__) |
|
32 | 31 | |
@@ -128,7 +127,7 b' class AuthnRootResource(AuthnResourceBas' | |||
|
128 | 127 | # Allow plugin resources with identical names by rename duplicates. |
|
129 | 128 | unique_name = _ensure_unique_name(resource.__name__) |
|
130 | 129 | if unique_name != resource.__name__: |
|
131 |
log.warn('Name collision for traversal resource "%s" registered' |
|
|
130 | log.warn('Name collision for traversal resource "%s" registered ' | |
|
132 | 131 | 'by authentication plugin "%s"', resource.__name__, |
|
133 | 132 | plugin_id) |
|
134 | 133 | resource.__name__ = unique_name |
@@ -20,9 +20,7 b'' | |||
|
20 | 20 | |
|
21 | 21 | import colander |
|
22 | 22 | |
|
23 | from pyramid.i18n import TranslationStringFactory | |
|
24 | ||
|
25 | _ = TranslationStringFactory('rhodecode-enterprise') | |
|
23 | from rhodecode.translation import _ | |
|
26 | 24 | |
|
27 | 25 | |
|
28 | 26 | class AuthnPluginSettingsSchemaBase(colander.MappingSchema): |
@@ -23,7 +23,6 b' import formencode.htmlfill' | |||
|
23 | 23 | import logging |
|
24 | 24 | |
|
25 | 25 | from pyramid.httpexceptions import HTTPFound |
|
26 | from pyramid.i18n import TranslationStringFactory | |
|
27 | 26 | from pyramid.renderers import render |
|
28 | 27 | from pyramid.response import Response |
|
29 | 28 | |
@@ -34,11 +33,10 b' from rhodecode.lib.auth import LoginRequ' | |||
|
34 | 33 | from rhodecode.model.forms import AuthSettingsForm |
|
35 | 34 | from rhodecode.model.meta import Session |
|
36 | 35 | from rhodecode.model.settings import SettingsModel |
|
36 | from rhodecode.translation import _ | |
|
37 | 37 | |
|
38 | 38 | log = logging.getLogger(__name__) |
|
39 | 39 | |
|
40 | _ = TranslationStringFactory('rhodecode-enterprise') | |
|
41 | ||
|
42 | 40 | |
|
43 | 41 | class AuthnPluginViewBase(object): |
|
44 | 42 | |
@@ -47,51 +45,27 b' class AuthnPluginViewBase(object):' | |||
|
47 | 45 | self.context = context |
|
48 | 46 | self.plugin = context.plugin |
|
49 | 47 | |
|
50 | # TODO: Think about replacing the htmlfill stuff. | |
|
51 | def _render_and_fill(self, template, template_context, request, | |
|
52 | form_defaults, validation_errors): | |
|
53 | """ | |
|
54 | Helper to render a template and fill the HTML form fields with | |
|
55 | defaults. Also displays the form errors. | |
|
56 | """ | |
|
57 | # Render template to string. | |
|
58 | html = render(template, template_context, request=request) | |
|
59 | ||
|
60 | # Fill the HTML form fields with default values and add error messages. | |
|
61 | html = formencode.htmlfill.render( | |
|
62 | html, | |
|
63 | defaults=form_defaults, | |
|
64 | errors=validation_errors, | |
|
65 | prefix_error=False, | |
|
66 | encoding="UTF-8", | |
|
67 | force_defaults=False) | |
|
68 | ||
|
69 | return html | |
|
70 | ||
|
71 | def settings_get(self): | |
|
48 | def settings_get(self, defaults=None, errors=None): | |
|
72 | 49 | """ |
|
73 | 50 | View that displays the plugin settings as a form. |
|
74 | 51 | """ |
|
75 |
|
|
|
76 | validation_errors = None | |
|
52 | defaults = defaults or {} | |
|
53 | errors = errors or {} | |
|
77 | 54 | schema = self.plugin.get_settings_schema() |
|
78 | 55 | |
|
79 | 56 | # Get default values for the form. |
|
80 |
for node in schema |
|
|
81 |
value = self.plugin.get_setting_by_name(node.name) |
|
|
82 |
|
|
|
57 | for node in schema: | |
|
58 | db_value = self.plugin.get_setting_by_name(node.name) | |
|
59 | defaults.setdefault(node.name, db_value) | |
|
83 | 60 | |
|
84 | 61 | template_context = { |
|
62 | 'defaults': defaults, | |
|
63 | 'errors': errors, | |
|
64 | 'plugin': self.context.plugin, | |
|
85 | 65 | 'resource': self.context, |
|
86 | 'plugin': self.context.plugin | |
|
87 | 66 | } |
|
88 | 67 | |
|
89 | return Response(self._render_and_fill( | |
|
90 | 'rhodecode:templates/admin/auth/plugin_settings.html', | |
|
91 | template_context, | |
|
92 | self.request, | |
|
93 | form_defaults, | |
|
94 | validation_errors)) | |
|
68 | return template_context | |
|
95 | 69 | |
|
96 | 70 | def settings_post(self): |
|
97 | 71 | """ |
@@ -102,24 +76,12 b' class AuthnPluginViewBase(object):' | |||
|
102 | 76 | valid_data = schema.deserialize(self.request.params) |
|
103 | 77 | except colander.Invalid, e: |
|
104 | 78 | # Display error message and display form again. |
|
105 | form_defaults = self.request.params | |
|
106 | validation_errors = e.asdict() | |
|
107 | 79 | self.request.session.flash( |
|
108 | 80 | _('Errors exist when saving plugin settings. ' |
|
109 |
|
|
|
81 | 'Please check the form inputs.'), | |
|
110 | 82 | queue='error') |
|
111 | ||
|
112 | template_context = { | |
|
113 | 'resource': self.context, | |
|
114 | 'plugin': self.context.plugin | |
|
115 | } | |
|
116 | ||
|
117 | return Response(self._render_and_fill( | |
|
118 | 'rhodecode:templates/admin/auth/plugin_settings.html', | |
|
119 | template_context, | |
|
120 | self.request, | |
|
121 | form_defaults, | |
|
122 | validation_errors)) | |
|
83 | defaults = schema.flatten(self.request.params) | |
|
84 | return self.settings_get(errors=e.asdict(), defaults=defaults) | |
|
123 | 85 | |
|
124 | 86 | # Store validated data. |
|
125 | 87 | for name, value in valid_data.items(): |
@@ -151,10 +113,10 b' class AuthSettingsView(object):' | |||
|
151 | 113 | |
|
152 | 114 | @LoginRequired() |
|
153 | 115 | @HasPermissionAllDecorator('hg.admin') |
|
154 |
def index(self, defaults= |
|
|
116 | def index(self, defaults=None, errors=None, prefix_error=False): | |
|
117 | defaults = defaults or {} | |
|
155 | 118 | authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry) |
|
156 | default_plugins = ['egg:rhodecode-enterprise-ce#rhodecode'] | |
|
157 | enabled_plugins = SettingsModel().get_auth_plugins() or default_plugins | |
|
119 | enabled_plugins = SettingsModel().get_auth_plugins() | |
|
158 | 120 | |
|
159 | 121 | # Create template context and render it. |
|
160 | 122 | template_context = { |
@@ -27,10 +27,12 b' import logging' | |||
|
27 | 27 | import rhodecode |
|
28 | 28 | import platform |
|
29 | 29 | import re |
|
30 | import io | |
|
30 | 31 | |
|
31 | 32 | from mako.lookup import TemplateLookup |
|
32 | 33 | from pylons.configuration import PylonsConfig |
|
33 | 34 | from pylons.error import handle_mako_error |
|
35 | from pyramid.settings import asbool | |
|
34 | 36 | |
|
35 | 37 | # don't remove this import it does magic for celery |
|
36 | 38 | from rhodecode.lib import celerypylons # noqa |
@@ -39,6 +41,7 b' import rhodecode.lib.app_globals as app_' | |||
|
39 | 41 | |
|
40 | 42 | from rhodecode.config import utils |
|
41 | 43 | from rhodecode.config.routing import make_map |
|
44 | from rhodecode.config.jsroutes import generate_jsroutes_content | |
|
42 | 45 | |
|
43 | 46 | from rhodecode.lib import helpers |
|
44 | 47 | from rhodecode.lib.auth import set_available_permissions |
@@ -51,7 +54,6 b' from rhodecode.model.scm import ScmModel' | |||
|
51 | 54 | |
|
52 | 55 | log = logging.getLogger(__name__) |
|
53 | 56 | |
|
54 | ||
|
55 | 57 | def load_environment(global_conf, app_conf, initial=False, |
|
56 | 58 | test_env=None, test_index=None): |
|
57 | 59 | """ |
@@ -60,7 +62,6 b' def load_environment(global_conf, app_co' | |||
|
60 | 62 | """ |
|
61 | 63 | config = PylonsConfig() |
|
62 | 64 | |
|
63 | rhodecode.is_test = str2bool(app_conf.get('is_test', 'False')) | |
|
64 | 65 | |
|
65 | 66 | # Pylons paths |
|
66 | 67 | root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
@@ -80,6 +81,16 b' def load_environment(global_conf, app_co' | |||
|
80 | 81 | config['app_conf'].get('celery.always.eager')) |
|
81 | 82 | |
|
82 | 83 | config['routes.map'] = make_map(config) |
|
84 | ||
|
85 | if asbool(config['debug']): | |
|
86 | jsroutes = config['routes.map'].jsroutes() | |
|
87 | jsroutes_file_content = generate_jsroutes_content(jsroutes) | |
|
88 | jsroutes_file_path = os.path.join( | |
|
89 | paths['static_files'], 'js', 'rhodecode', 'routes.js') | |
|
90 | ||
|
91 | with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f: | |
|
92 | f.write(jsroutes_file_content) | |
|
93 | ||
|
83 | 94 | config['pylons.app_globals'] = app_globals.Globals(config) |
|
84 | 95 | config['pylons.h'] = helpers |
|
85 | 96 | rhodecode.CONFIG = config |
@@ -100,18 +111,6 b' def load_environment(global_conf, app_co' | |||
|
100 | 111 | |
|
101 | 112 | # sets the c attribute access when don't existing attribute are accessed |
|
102 | 113 | config['pylons.strict_tmpl_context'] = True |
|
103 | config_file_name = os.path.split(config['__file__'])[-1] | |
|
104 | test = re.match('^test[\w_]*\.ini$', config_file_name) is not None | |
|
105 | if test: | |
|
106 | if test_env is None: | |
|
107 | test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0)) | |
|
108 | ||
|
109 | from rhodecode.lib.utils import create_test_env, create_test_index | |
|
110 | from rhodecode.tests import TESTS_TMP_PATH | |
|
111 | # test repos | |
|
112 | if test_env: | |
|
113 | create_test_env(TESTS_TMP_PATH, config) | |
|
114 | create_test_index(TESTS_TMP_PATH, config, True) | |
|
115 | 114 | |
|
116 | 115 | # Limit backends to "vcs.backends" from configuration |
|
117 | 116 | backends = config['vcs.backends'] = aslist( |
@@ -133,10 +132,6 b' def load_environment(global_conf, app_co' | |||
|
133 | 132 | protocol=utils.get_vcs_server_protocol(config), |
|
134 | 133 | log_level=config['vcs.server.log_level']) |
|
135 | 134 | |
|
136 | # MULTIPLE DB configs | |
|
137 | # Setup the SQLAlchemy database engine | |
|
138 | utils.initialize_database(config) | |
|
139 | ||
|
140 | 135 | set_available_permissions(config) |
|
141 | 136 | db_cfg = make_db_config(clear_session=True) |
|
142 | 137 | |
@@ -179,3 +174,19 b' def _use_direct_hook_calls(config):' | |||
|
179 | 174 | def _get_vcs_hooks_protocol(config): |
|
180 | 175 | protocol = config.get('vcs.hooks.protocol', 'pyro4').lower() |
|
181 | 176 | return protocol |
|
177 | ||
|
178 | ||
|
179 | def load_pyramid_environment(global_config, settings): | |
|
180 | # Some parts of the code expect a merge of global and app settings. | |
|
181 | settings_merged = global_config.copy() | |
|
182 | settings_merged.update(settings) | |
|
183 | ||
|
184 | # If this is a test run we prepare the test environment like | |
|
185 | # creating a test database, test search index and test repositories. | |
|
186 | # This has to be done before the database connection is initialized. | |
|
187 | if settings['is_test']: | |
|
188 | rhodecode.is_test = True | |
|
189 | utils.initialize_test_environment(settings_merged) | |
|
190 | ||
|
191 | # Initialize the database connection. | |
|
192 | utils.initialize_database(settings_merged) |
@@ -37,7 +37,8 b' import routes.util' | |||
|
37 | 37 | |
|
38 | 38 | import rhodecode |
|
39 | 39 | from rhodecode.config import patches |
|
40 |
from rhodecode.config.environment import |
|
|
40 | from rhodecode.config.environment import ( | |
|
41 | load_environment, load_pyramid_environment) | |
|
41 | 42 | from rhodecode.lib.middleware import csrf |
|
42 | 43 | from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled |
|
43 | 44 | from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper |
@@ -160,6 +161,9 b' def make_pyramid_app(global_config, **se' | |||
|
160 | 161 | sanitize_settings_and_apply_defaults(settings) |
|
161 | 162 | config = Configurator(settings=settings) |
|
162 | 163 | add_pylons_compat_data(config.registry, global_config, settings_pylons) |
|
164 | ||
|
165 | load_pyramid_environment(global_config, settings) | |
|
166 | ||
|
163 | 167 | includeme(config) |
|
164 | 168 | includeme_last(config) |
|
165 | 169 | pyramid_app = config.make_wsgi_app() |
@@ -182,6 +186,7 b' def includeme(config):' | |||
|
182 | 186 | config.include('pyramid_mako') |
|
183 | 187 | config.include('pyramid_beaker') |
|
184 | 188 | config.include('rhodecode.authentication') |
|
189 | config.include('rhodecode.login') | |
|
185 | 190 | config.include('rhodecode.tweens') |
|
186 | 191 | config.include('rhodecode.api') |
|
187 | 192 | |
@@ -301,6 +306,7 b' def sanitize_settings_and_apply_defaults' | |||
|
301 | 306 | |
|
302 | 307 | _bool_setting(settings, 'vcs.server.enable', 'true') |
|
303 | 308 | _bool_setting(settings, 'static_files', 'true') |
|
309 | _bool_setting(settings, 'is_test', 'false') | |
|
304 | 310 | |
|
305 | 311 | return settings |
|
306 | 312 |
@@ -29,6 +29,7 b' IMPORTANT: if you change any routing her' | |||
|
29 | 29 | and _route_name variable which uses some of stored naming here to do redirects. |
|
30 | 30 | """ |
|
31 | 31 | import os |
|
32 | import re | |
|
32 | 33 | from routes import Mapper |
|
33 | 34 | |
|
34 | 35 | from rhodecode.config import routing_links |
@@ -50,9 +51,60 b' URL_NAME_REQUIREMENTS = {' | |||
|
50 | 51 | } |
|
51 | 52 | |
|
52 | 53 | |
|
54 | class JSRoutesMapper(Mapper): | |
|
55 | """ | |
|
56 | Wrapper for routes.Mapper to make pyroutes compatible url definitions | |
|
57 | """ | |
|
58 | _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$') | |
|
59 | _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)') | |
|
60 | def __init__(self, *args, **kw): | |
|
61 | super(JSRoutesMapper, self).__init__(*args, **kw) | |
|
62 | self._jsroutes = [] | |
|
63 | ||
|
64 | def connect(self, *args, **kw): | |
|
65 | """ | |
|
66 | Wrapper for connect to take an extra argument jsroute=True | |
|
67 | ||
|
68 | :param jsroute: boolean, if True will add the route to the pyroutes list | |
|
69 | """ | |
|
70 | if kw.pop('jsroute', False): | |
|
71 | if not self._named_route_regex.match(args[0]): | |
|
72 | raise Exception('only named routes can be added to pyroutes') | |
|
73 | self._jsroutes.append(args[0]) | |
|
74 | ||
|
75 | super(JSRoutesMapper, self).connect(*args, **kw) | |
|
76 | ||
|
77 | def _extract_route_information(self, route): | |
|
78 | """ | |
|
79 | Convert a route into tuple(name, path, args), eg: | |
|
80 | ('user_profile', '/profile/%(username)s', ['username']) | |
|
81 | """ | |
|
82 | routepath = route.routepath | |
|
83 | def replace(matchobj): | |
|
84 | if matchobj.group(1): | |
|
85 | return "%%(%s)s" % matchobj.group(1).split(':')[0] | |
|
86 | else: | |
|
87 | return "%%(%s)s" % matchobj.group(2) | |
|
88 | ||
|
89 | routepath = self._argument_prog.sub(replace, routepath) | |
|
90 | return ( | |
|
91 | route.name, | |
|
92 | routepath, | |
|
93 | [(arg[0].split(':')[0] if arg[0] != '' else arg[1]) | |
|
94 | for arg in self._argument_prog.findall(route.routepath)] | |
|
95 | ) | |
|
96 | ||
|
97 | def jsroutes(self): | |
|
98 | """ | |
|
99 | Return a list of pyroutes.js compatible routes | |
|
100 | """ | |
|
101 | for route_name in self._jsroutes: | |
|
102 | yield self._extract_route_information(self._routenames[route_name]) | |
|
103 | ||
|
104 | ||
|
53 | 105 | def make_map(config): |
|
54 | 106 | """Create, configure and return the routes Mapper""" |
|
55 | rmap = Mapper(directory=config['pylons.paths']['controllers'], | |
|
107 | rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'], | |
|
56 | 108 | always_scan=config['debug']) |
|
57 | 109 | rmap.minimization = False |
|
58 | 110 | rmap.explicit = False |
@@ -124,14 +176,14 b' def make_map(config):' | |||
|
124 | 176 | #========================================================================== |
|
125 | 177 | |
|
126 | 178 | # MAIN PAGE |
|
127 | rmap.connect('home', '/', controller='home', action='index') | |
|
128 |
rmap.connect(' |
|
|
129 |
action=' |
|
|
179 | rmap.connect('home', '/', controller='home', action='index', jsroute=True) | |
|
180 | rmap.connect('goto_switcher_data', '/_goto_data', controller='home', | |
|
181 | action='goto_switcher_data') | |
|
130 | 182 | rmap.connect('repo_list_data', '/_repos', controller='home', |
|
131 | 183 | action='repo_list_data') |
|
132 | 184 | |
|
133 | 185 | rmap.connect('user_autocomplete_data', '/_users', controller='home', |
|
134 | action='user_autocomplete_data') | |
|
186 | action='user_autocomplete_data', jsroute=True) | |
|
135 | 187 | rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home', |
|
136 | 188 | action='user_group_autocomplete_data') |
|
137 | 189 | |
@@ -167,7 +219,7 b' def make_map(config):' | |||
|
167 | 219 | action='create', conditions={'method': ['POST']}) |
|
168 | 220 | m.connect('repos', '/repos', |
|
169 | 221 | action='index', conditions={'method': ['GET']}) |
|
170 | m.connect('new_repo', '/create_repository', | |
|
222 | m.connect('new_repo', '/create_repository', jsroute=True, | |
|
171 | 223 | action='create_repository', conditions={'method': ['GET']}) |
|
172 | 224 | m.connect('/repos/{repo_name}', |
|
173 | 225 | action='update', conditions={'method': ['PUT'], |
@@ -303,22 +355,29 b' def make_map(config):' | |||
|
303 | 355 | function=check_user_group) |
|
304 | 356 | |
|
305 | 357 | # EXTRAS USER GROUP ROUTES |
|
306 |
m.connect('edit_user_group_global_perms', |
|
|
358 | m.connect('edit_user_group_global_perms', | |
|
359 | '/user_groups/{user_group_id}/edit/global_permissions', | |
|
307 | 360 | action='edit_global_perms', conditions={'method': ['GET']}) |
|
308 |
m.connect('edit_user_group_global_perms', |
|
|
361 | m.connect('edit_user_group_global_perms', | |
|
362 | '/user_groups/{user_group_id}/edit/global_permissions', | |
|
309 | 363 | action='update_global_perms', conditions={'method': ['PUT']}) |
|
310 |
m.connect('edit_user_group_perms_summary', |
|
|
364 | m.connect('edit_user_group_perms_summary', | |
|
365 | '/user_groups/{user_group_id}/edit/permissions_summary', | |
|
311 | 366 | action='edit_perms_summary', conditions={'method': ['GET']}) |
|
312 | 367 | |
|
313 |
m.connect('edit_user_group_perms', |
|
|
368 | m.connect('edit_user_group_perms', | |
|
369 | '/user_groups/{user_group_id}/edit/permissions', | |
|
314 | 370 | action='edit_perms', conditions={'method': ['GET']}) |
|
315 |
m.connect('edit_user_group_perms', |
|
|
371 | m.connect('edit_user_group_perms', | |
|
372 | '/user_groups/{user_group_id}/edit/permissions', | |
|
316 | 373 | action='update_perms', conditions={'method': ['PUT']}) |
|
317 | 374 | |
|
318 |
m.connect('edit_user_group_advanced', |
|
|
375 | m.connect('edit_user_group_advanced', | |
|
376 | '/user_groups/{user_group_id}/edit/advanced', | |
|
319 | 377 | action='edit_advanced', conditions={'method': ['GET']}) |
|
320 | 378 | |
|
321 |
m.connect('edit_user_group_members', |
|
|
379 | m.connect('edit_user_group_members', | |
|
380 | '/user_groups/{user_group_id}/edit/members', jsroute=True, | |
|
322 | 381 | action='edit_members', conditions={'method': ['GET']}) |
|
323 | 382 | |
|
324 | 383 | # ADMIN PERMISSIONS ROUTES |
@@ -496,12 +555,6 b' def make_map(config):' | |||
|
496 | 555 | m.connect('my_account_auth_tokens', '/my_account/auth_tokens', |
|
497 | 556 | action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']}) |
|
498 | 557 | |
|
499 | m.connect('my_account_oauth', '/my_account/oauth', | |
|
500 | action='my_account_oauth', conditions={'method': ['GET']}) | |
|
501 | m.connect('my_account_oauth', '/my_account/oauth', | |
|
502 | action='my_account_oauth_delete', | |
|
503 | conditions={'method': ['DELETE']}) | |
|
504 | ||
|
505 | 558 | # NOTIFICATION REST ROUTES |
|
506 | 559 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
507 | 560 | controller='admin/notifications') as m: |
@@ -522,9 +575,9 b' def make_map(config):' | |||
|
522 | 575 | controller='admin/gists') as m: |
|
523 | 576 | m.connect('gists', '/gists', |
|
524 | 577 | action='create', conditions={'method': ['POST']}) |
|
525 | m.connect('gists', '/gists', | |
|
578 | m.connect('gists', '/gists', jsroute=True, | |
|
526 | 579 | action='index', conditions={'method': ['GET']}) |
|
527 | m.connect('new_gist', '/gists/new', | |
|
580 | m.connect('new_gist', '/gists/new', jsroute=True, | |
|
528 | 581 | action='new', conditions={'method': ['GET']}) |
|
529 | 582 | |
|
530 | 583 | m.connect('/gists/{gist_id}', |
@@ -557,8 +610,12 b' def make_map(config):' | |||
|
557 | 610 | m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', |
|
558 | 611 | action='add_repo') |
|
559 | 612 | m.connect( |
|
560 | 'pull_requests_global', '/pull_requests/{pull_request_id:[0-9]+}', | |
|
613 | 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}', | |
|
561 | 614 | action='pull_requests') |
|
615 | m.connect( | |
|
616 | 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}', | |
|
617 | action='pull_requests') | |
|
618 | ||
|
562 | 619 | |
|
563 | 620 | # USER JOURNAL |
|
564 | 621 | rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), |
@@ -586,7 +643,7 b' def make_map(config):' | |||
|
586 | 643 | action='public_journal_atom') |
|
587 | 644 | |
|
588 | 645 | rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,), |
|
589 | controller='journal', action='toggle_following', | |
|
646 | controller='journal', action='toggle_following', jsroute=True, | |
|
590 | 647 | conditions={'method': ['POST']}) |
|
591 | 648 | |
|
592 | 649 | # FULL TEXT SEARCH |
@@ -598,27 +655,6 b' def make_map(config):' | |||
|
598 | 655 | conditions={'function': check_repo}, |
|
599 | 656 | requirements=URL_NAME_REQUIREMENTS) |
|
600 | 657 | |
|
601 | # LOGIN/LOGOUT/REGISTER/SIGN IN | |
|
602 | rmap.connect('login_home', '%s/login' % (ADMIN_PREFIX,), controller='login', | |
|
603 | action='index') | |
|
604 | ||
|
605 | rmap.connect('logout_home', '%s/logout' % (ADMIN_PREFIX,), controller='login', | |
|
606 | action='logout', conditions={'method': ['POST']}) | |
|
607 | ||
|
608 | rmap.connect('register', '%s/register' % (ADMIN_PREFIX,), controller='login', | |
|
609 | action='register') | |
|
610 | ||
|
611 | rmap.connect('reset_password', '%s/password_reset' % (ADMIN_PREFIX,), | |
|
612 | controller='login', action='password_reset') | |
|
613 | ||
|
614 | rmap.connect('reset_password_confirmation', | |
|
615 | '%s/password_reset_confirmation' % (ADMIN_PREFIX,), | |
|
616 | controller='login', action='password_reset_confirmation') | |
|
617 | ||
|
618 | rmap.connect('social_auth', | |
|
619 | '%s/social_auth/{provider_name}' % (ADMIN_PREFIX,), | |
|
620 | controller='login', action='social_auth') | |
|
621 | ||
|
622 | 658 | # FEEDS |
|
623 | 659 | rmap.connect('rss_feed_home', '/{repo_name}/feed/rss', |
|
624 | 660 | controller='feed', action='rss', |
@@ -644,17 +680,17 b' def make_map(config):' | |||
|
644 | 680 | rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}', |
|
645 | 681 | controller='summary', action='repo_stats', |
|
646 | 682 | conditions={'function': check_repo}, |
|
647 | requirements=URL_NAME_REQUIREMENTS) | |
|
683 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
648 | 684 | |
|
649 | 685 | rmap.connect('repo_refs_data', '/{repo_name}/refs-data', |
|
650 | controller='summary', action='repo_refs_data', | |
|
686 | controller='summary', action='repo_refs_data', jsroute=True, | |
|
651 | 687 | requirements=URL_NAME_REQUIREMENTS) |
|
652 | 688 | rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog', |
|
653 | 689 | controller='summary', action='repo_refs_changelog_data', |
|
654 | requirements=URL_NAME_REQUIREMENTS) | |
|
690 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
655 | 691 | |
|
656 | 692 | rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}', |
|
657 | controller='changeset', revision='tip', | |
|
693 | controller='changeset', revision='tip', jsroute=True, | |
|
658 | 694 | conditions={'function': check_repo}, |
|
659 | 695 | requirements=URL_NAME_REQUIREMENTS) |
|
660 | 696 | rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}', |
@@ -667,12 +703,13 b' def make_map(config):' | |||
|
667 | 703 | requirements=URL_NAME_REQUIREMENTS) |
|
668 | 704 | |
|
669 | 705 | # repo edit options |
|
670 | rmap.connect('edit_repo', '/{repo_name}/settings', | |
|
706 | rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True, | |
|
671 | 707 | controller='admin/repos', action='edit', |
|
672 | 708 | conditions={'method': ['GET'], 'function': check_repo}, |
|
673 | 709 | requirements=URL_NAME_REQUIREMENTS) |
|
674 | 710 | |
|
675 | 711 | rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions', |
|
712 | jsroute=True, | |
|
676 | 713 | controller='admin/repos', action='edit_permissions', |
|
677 | 714 | conditions={'method': ['GET'], 'function': check_repo}, |
|
678 | 715 | requirements=URL_NAME_REQUIREMENTS) |
@@ -804,13 +841,13 b' def make_map(config):' | |||
|
804 | 841 | requirements=URL_NAME_REQUIREMENTS) |
|
805 | 842 | |
|
806 | 843 | rmap.connect('changeset_comment', |
|
807 | '/{repo_name}/changeset/{revision}/comment', | |
|
844 | '/{repo_name}/changeset/{revision}/comment', jsroute=True, | |
|
808 | 845 | controller='changeset', revision='tip', action='comment', |
|
809 | 846 | conditions={'function': check_repo}, |
|
810 | 847 | requirements=URL_NAME_REQUIREMENTS) |
|
811 | 848 | |
|
812 | 849 | rmap.connect('changeset_comment_preview', |
|
813 | '/{repo_name}/changeset/comment/preview', | |
|
850 | '/{repo_name}/changeset/comment/preview', jsroute=True, | |
|
814 | 851 | controller='changeset', action='preview_comment', |
|
815 | 852 | conditions={'function': check_repo, 'method': ['POST']}, |
|
816 | 853 | requirements=URL_NAME_REQUIREMENTS) |
@@ -819,11 +856,11 b' def make_map(config):' | |||
|
819 | 856 | '/{repo_name}/changeset/comment/{comment_id}/delete', |
|
820 | 857 | controller='changeset', action='delete_comment', |
|
821 | 858 | conditions={'function': check_repo, 'method': ['DELETE']}, |
|
822 | requirements=URL_NAME_REQUIREMENTS) | |
|
859 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
823 | 860 | |
|
824 | 861 | rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}', |
|
825 | 862 | controller='changeset', action='changeset_info', |
|
826 | requirements=URL_NAME_REQUIREMENTS) | |
|
863 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
827 | 864 | |
|
828 | 865 | rmap.connect('compare_home', |
|
829 | 866 | '/{repo_name}/compare', |
@@ -835,33 +872,33 b' def make_map(config):' | |||
|
835 | 872 | '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', |
|
836 | 873 | controller='compare', action='compare', |
|
837 | 874 | conditions={'function': check_repo}, |
|
838 | requirements=URL_NAME_REQUIREMENTS) | |
|
875 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
839 | 876 | |
|
840 | 877 | rmap.connect('pullrequest_home', |
|
841 | 878 | '/{repo_name}/pull-request/new', controller='pullrequests', |
|
842 | 879 | action='index', conditions={'function': check_repo, |
|
843 | 880 | 'method': ['GET']}, |
|
844 | requirements=URL_NAME_REQUIREMENTS) | |
|
881 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
845 | 882 | |
|
846 | 883 | rmap.connect('pullrequest', |
|
847 | 884 | '/{repo_name}/pull-request/new', controller='pullrequests', |
|
848 | 885 | action='create', conditions={'function': check_repo, |
|
849 | 886 | 'method': ['POST']}, |
|
850 | requirements=URL_NAME_REQUIREMENTS) | |
|
887 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
851 | 888 | |
|
852 | 889 | rmap.connect('pullrequest_repo_refs', |
|
853 | 890 | '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}', |
|
854 | 891 | controller='pullrequests', |
|
855 | 892 | action='get_repo_refs', |
|
856 | 893 | conditions={'function': check_repo, 'method': ['GET']}, |
|
857 | requirements=URL_NAME_REQUIREMENTS) | |
|
894 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
858 | 895 | |
|
859 | 896 | rmap.connect('pullrequest_repo_destinations', |
|
860 | 897 | '/{repo_name}/pull-request/repo-destinations', |
|
861 | 898 | controller='pullrequests', |
|
862 | 899 | action='get_repo_destinations', |
|
863 | 900 | conditions={'function': check_repo, 'method': ['GET']}, |
|
864 | requirements=URL_NAME_REQUIREMENTS) | |
|
901 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
865 | 902 | |
|
866 | 903 | rmap.connect('pullrequest_show', |
|
867 | 904 | '/{repo_name}/pull-request/{pull_request_id}', |
@@ -875,7 +912,7 b' def make_map(config):' | |||
|
875 | 912 | controller='pullrequests', |
|
876 | 913 | action='update', conditions={'function': check_repo, |
|
877 | 914 | 'method': ['PUT']}, |
|
878 | requirements=URL_NAME_REQUIREMENTS) | |
|
915 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
879 | 916 | |
|
880 | 917 | rmap.connect('pullrequest_merge', |
|
881 | 918 | '/{repo_name}/pull-request/{pull_request_id}', |
@@ -896,20 +933,20 b' def make_map(config):' | |||
|
896 | 933 | controller='pullrequests', |
|
897 | 934 | action='show_all', conditions={'function': check_repo, |
|
898 | 935 | 'method': ['GET']}, |
|
899 | requirements=URL_NAME_REQUIREMENTS) | |
|
936 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
900 | 937 | |
|
901 | 938 | rmap.connect('pullrequest_comment', |
|
902 | 939 | '/{repo_name}/pull-request-comment/{pull_request_id}', |
|
903 | 940 | controller='pullrequests', |
|
904 | 941 | action='comment', conditions={'function': check_repo, |
|
905 | 942 | 'method': ['POST']}, |
|
906 | requirements=URL_NAME_REQUIREMENTS) | |
|
943 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
907 | 944 | |
|
908 | 945 | rmap.connect('pullrequest_comment_delete', |
|
909 | 946 | '/{repo_name}/pull-request-comment/{comment_id}/delete', |
|
910 | 947 | controller='pullrequests', action='delete_comment', |
|
911 | 948 | conditions={'function': check_repo, 'method': ['DELETE']}, |
|
912 | requirements=URL_NAME_REQUIREMENTS) | |
|
949 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
913 | 950 | |
|
914 | 951 | rmap.connect('summary_home_explicit', '/{repo_name}/summary', |
|
915 | 952 | controller='summary', conditions={'function': check_repo}, |
@@ -927,7 +964,7 b' def make_map(config):' | |||
|
927 | 964 | controller='bookmarks', conditions={'function': check_repo}, |
|
928 | 965 | requirements=URL_NAME_REQUIREMENTS) |
|
929 | 966 | |
|
930 | rmap.connect('changelog_home', '/{repo_name}/changelog', | |
|
967 | rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True, | |
|
931 | 968 | controller='changelog', conditions={'function': check_repo}, |
|
932 | 969 | requirements=URL_NAME_REQUIREMENTS) |
|
933 | 970 | |
@@ -936,21 +973,21 b' def make_map(config):' | |||
|
936 | 973 | conditions={'function': check_repo}, |
|
937 | 974 | requirements=URL_NAME_REQUIREMENTS) |
|
938 | 975 | |
|
939 |
rmap.connect('changelog_file_home', |
|
|
976 | rmap.connect('changelog_file_home', | |
|
977 | '/{repo_name}/changelog/{revision}/{f_path}', | |
|
940 | 978 | controller='changelog', f_path=None, |
|
941 | 979 | conditions={'function': check_repo}, |
|
942 | requirements=URL_NAME_REQUIREMENTS) | |
|
980 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
943 | 981 | |
|
944 | 982 | rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}', |
|
945 | 983 | controller='changelog', action='changelog_details', |
|
946 | 984 | conditions={'function': check_repo}, |
|
947 | 985 | requirements=URL_NAME_REQUIREMENTS) |
|
948 | 986 | |
|
949 | rmap.connect('files_home', | |
|
950 | '/{repo_name}/files/{revision}/{f_path}', | |
|
987 | rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}', | |
|
951 | 988 | controller='files', revision='tip', f_path='', |
|
952 | 989 | conditions={'function': check_repo}, |
|
953 | requirements=URL_NAME_REQUIREMENTS) | |
|
990 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
954 | 991 | |
|
955 | 992 | rmap.connect('files_home_simple_catchrev', |
|
956 | 993 | '/{repo_name}/files/{revision}', |
@@ -968,13 +1005,13 b' def make_map(config):' | |||
|
968 | 1005 | '/{repo_name}/history/{revision}/{f_path}', |
|
969 | 1006 | controller='files', action='history', revision='tip', f_path='', |
|
970 | 1007 | conditions={'function': check_repo}, |
|
971 | requirements=URL_NAME_REQUIREMENTS) | |
|
1008 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
972 | 1009 | |
|
973 | 1010 | rmap.connect('files_authors_home', |
|
974 | 1011 | '/{repo_name}/authors/{revision}/{f_path}', |
|
975 | 1012 | controller='files', action='authors', revision='tip', f_path='', |
|
976 | 1013 | conditions={'function': check_repo}, |
|
977 | requirements=URL_NAME_REQUIREMENTS) | |
|
1014 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
978 | 1015 | |
|
979 | 1016 | rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}', |
|
980 | 1017 | controller='files', action='diff', f_path='', |
@@ -1053,19 +1090,19 b' def make_map(config):' | |||
|
1053 | 1090 | rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}', |
|
1054 | 1091 | controller='files', action='archivefile', |
|
1055 | 1092 | conditions={'function': check_repo}, |
|
1056 | requirements=URL_NAME_REQUIREMENTS) | |
|
1093 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
1057 | 1094 | |
|
1058 | 1095 | rmap.connect('files_nodelist_home', |
|
1059 | 1096 | '/{repo_name}/nodelist/{revision}/{f_path}', |
|
1060 | 1097 | controller='files', action='nodelist', |
|
1061 | 1098 | conditions={'function': check_repo}, |
|
1062 | requirements=URL_NAME_REQUIREMENTS) | |
|
1099 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
1063 | 1100 | |
|
1064 | 1101 | rmap.connect('files_metadata_list_home', |
|
1065 | 1102 | '/{repo_name}/metadata_list/{revision}/{f_path}', |
|
1066 | 1103 | controller='files', action='metadata_list', |
|
1067 | 1104 | conditions={'function': check_repo}, |
|
1068 | requirements=URL_NAME_REQUIREMENTS) | |
|
1105 | requirements=URL_NAME_REQUIREMENTS, jsroute=True) | |
|
1069 | 1106 | |
|
1070 | 1107 | rmap.connect('repo_fork_create_home', '/{repo_name}/fork', |
|
1071 | 1108 | controller='forks', action='fork_create', |
@@ -1096,7 +1133,7 b' def make_map(config):' | |||
|
1096 | 1133 | |
|
1097 | 1134 | # catch all, at the end |
|
1098 | 1135 | _connect_with_slash( |
|
1099 | rmap, 'summary_home', '/{repo_name}', | |
|
1136 | rmap, 'summary_home', '/{repo_name}', jsroute=True, | |
|
1100 | 1137 | controller='summary', action='index', |
|
1101 | 1138 | conditions={'function': check_repo}, |
|
1102 | 1139 | requirements=URL_NAME_REQUIREMENTS) |
@@ -73,6 +73,18 b' def initialize_database(config):' | |||
|
73 | 73 | init_model(engine, encryption_key=config['beaker.session.secret']) |
|
74 | 74 | |
|
75 | 75 | |
|
76 | def initialize_test_environment(settings, test_env=None): | |
|
77 | if test_env is None: | |
|
78 | test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0)) | |
|
79 | ||
|
80 | from rhodecode.lib.utils import create_test_env, create_test_index | |
|
81 | from rhodecode.tests import TESTS_TMP_PATH | |
|
82 | # test repos | |
|
83 | if test_env: | |
|
84 | create_test_env(TESTS_TMP_PATH, settings) | |
|
85 | create_test_index(TESTS_TMP_PATH, settings, True) | |
|
86 | ||
|
87 | ||
|
76 | 88 | def get_vcs_server_protocol(config): |
|
77 | 89 | protocol = config.get('vcs.server.protocol', 'pyro4') |
|
78 | 90 | return protocol |
@@ -39,16 +39,15 b' from rhodecode.lib.auth import (' | |||
|
39 | 39 | from rhodecode.lib.base import BaseController, render |
|
40 | 40 | from rhodecode.lib.utils2 import safe_int, md5 |
|
41 | 41 | from rhodecode.lib.ext_json import json |
|
42 | from rhodecode.model.db import (Repository, PullRequest, PullRequestReviewers, | |
|
43 | UserEmailMap, User, UserFollowing, | |
|
44 | ExternalIdentity) | |
|
42 | from rhodecode.model.db import ( | |
|
43 | Repository, PullRequest, PullRequestReviewers, UserEmailMap, User, | |
|
44 | UserFollowing) | |
|
45 | 45 | from rhodecode.model.forms import UserForm, PasswordChangeForm |
|
46 | 46 | from rhodecode.model.scm import RepoList |
|
47 | 47 | from rhodecode.model.user import UserModel |
|
48 | 48 | from rhodecode.model.repo import RepoModel |
|
49 | 49 | from rhodecode.model.auth_token import AuthTokenModel |
|
50 | 50 | from rhodecode.model.meta import Session |
|
51 | from rhodecode.model.settings import SettingsModel | |
|
52 | 51 | |
|
53 | 52 | log = logging.getLogger(__name__) |
|
54 | 53 | |
@@ -347,27 +346,3 b' class MyAccountController(BaseController' | |||
|
347 | 346 | h.flash(_("Auth token successfully deleted"), category='success') |
|
348 | 347 | |
|
349 | 348 | return redirect(url('my_account_auth_tokens')) |
|
350 | ||
|
351 | def my_account_oauth(self): | |
|
352 | c.active = 'oauth' | |
|
353 | self.__load_data() | |
|
354 | c.user_oauth_tokens = ExternalIdentity().by_local_user_id( | |
|
355 | c.rhodecode_user.user_id).all() | |
|
356 | settings = SettingsModel().get_all_settings() | |
|
357 | c.social_plugins = SettingsModel().list_enabled_social_plugins( | |
|
358 | settings) | |
|
359 | return render('admin/my_account/my_account.html') | |
|
360 | ||
|
361 | @auth.CSRFRequired() | |
|
362 | def my_account_oauth_delete(self): | |
|
363 | token = ExternalIdentity.by_external_id_and_provider( | |
|
364 | request.params.get('external_id'), | |
|
365 | request.params.get('provider_name'), | |
|
366 | local_user_id=c.rhodecode_user.user_id | |
|
367 | ) | |
|
368 | if token: | |
|
369 | Session().delete(token) | |
|
370 | Session().commit() | |
|
371 | h.flash(_("OAuth token successfully deleted"), category='success') | |
|
372 | ||
|
373 | return redirect(url('my_account_oauth')) |
@@ -36,6 +36,7 b' from rhodecode.lib import auth' | |||
|
36 | 36 | from rhodecode.lib import helpers as h |
|
37 | 37 | from rhodecode.lib.exceptions import UserGroupAssignedException,\ |
|
38 | 38 | RepoGroupAssignmentError |
|
39 | from rhodecode.lib.utils import jsonify, action_logger | |
|
39 | 40 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int |
|
40 | 41 | from rhodecode.lib.auth import ( |
|
41 | 42 | LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, |
@@ -181,7 +182,8 b' class UserGroupsController(BaseControlle' | |||
|
181 | 182 | h.flash(_('Error occurred during creation of user group %s') \ |
|
182 | 183 | % request.POST.get('users_group_name'), category='error') |
|
183 | 184 | |
|
184 |
return redirect( |
|
|
185 | return redirect( | |
|
186 | url('edit_users_group', user_group_id=user_group.users_group_id)) | |
|
185 | 187 | |
|
186 | 188 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
187 | 189 | def new(self): |
@@ -467,5 +469,12 b' class UserGroupsController(BaseControlle' | |||
|
467 | 469 | c.group_members_obj = sorted((x.user for x in c.user_group.members), |
|
468 | 470 | key=lambda u: u.username.lower()) |
|
469 | 471 | |
|
470 |
|
|
|
472 | group_members = [(x.user_id, x.username) for x in c.group_members_obj] | |
|
473 | ||
|
474 | if request.is_xhr: | |
|
475 | return jsonify(lambda *a, **k: { | |
|
476 | 'members': group_members | |
|
477 | }) | |
|
478 | ||
|
479 | c.group_members = group_members | |
|
471 | 480 | return render('admin/user_groups/user_group_edit.html') |
@@ -198,7 +198,9 b' class CompareController(BaseRepoControll' | |||
|
198 | 198 | c.statuses = c.rhodecode_db_repo.statuses( |
|
199 | 199 | [x.raw_id for x in c.commit_ranges]) |
|
200 | 200 | |
|
201 | if partial: | |
|
201 | if partial: # for PR ajax commits loader | |
|
202 | if not c.ancestor: | |
|
203 | return '' # cannot merge if there is no ancestor | |
|
202 | 204 | return render('compare/compare_commits.html') |
|
203 | 205 | |
|
204 | 206 | if c.ancestor: |
@@ -24,16 +24,17 b' Home controller for RhodeCode Enterprise' | |||
|
24 | 24 | |
|
25 | 25 | import logging |
|
26 | 26 | import time |
|
27 | ||
|
27 | import re | |
|
28 | 28 | |
|
29 | from pylons import tmpl_context as c, request | |
|
29 | from pylons import tmpl_context as c, request, url, config | |
|
30 | 30 | from pylons.i18n.translation import _ |
|
31 | 31 | from sqlalchemy.sql import func |
|
32 | 32 | |
|
33 | 33 | from rhodecode.lib.auth import ( |
|
34 | LoginRequired, HasPermissionAllDecorator, | |
|
34 | LoginRequired, HasPermissionAllDecorator, AuthUser, | |
|
35 | 35 | HasRepoGroupPermissionAnyDecorator, XHRRequired) |
|
36 | 36 | from rhodecode.lib.base import BaseController, render |
|
37 | from rhodecode.lib.index import searcher_from_config | |
|
37 | 38 | from rhodecode.lib.ext_json import json |
|
38 | 39 | from rhodecode.lib.utils import jsonify |
|
39 | 40 | from rhodecode.lib.utils2 import safe_unicode |
@@ -134,7 +135,8 b' class HomeController(BaseController):' | |||
|
134 | 135 | 'id': obj['name'], |
|
135 | 136 | 'text': obj['name'], |
|
136 | 137 | 'type': 'repo', |
|
137 | 'obj': obj['dbrepo'] | |
|
138 | 'obj': obj['dbrepo'], | |
|
139 | 'url': url('summary_home', repo_name=obj['name']) | |
|
138 | 140 | } |
|
139 | 141 | for obj in repo_iter] |
|
140 | 142 | |
@@ -156,16 +158,45 b' class HomeController(BaseController):' | |||
|
156 | 158 | 'id': obj.group_name, |
|
157 | 159 | 'text': obj.group_name, |
|
158 | 160 | 'type': 'group', |
|
159 | 'obj': {} | |
|
161 | 'obj': {}, | |
|
162 | 'url': url('repo_group_home', group_name=obj.group_name) | |
|
160 | 163 | } |
|
161 | 164 | for obj in repo_groups_iter] |
|
162 | 165 | |
|
166 | def _get_hash_commit_list(self, hash_starts_with=None, limit=20): | |
|
167 | if not hash_starts_with or len(hash_starts_with) < 3: | |
|
168 | return [] | |
|
169 | ||
|
170 | commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) | |
|
171 | ||
|
172 | if len(commit_hashes) != 1: | |
|
173 | return [] | |
|
174 | ||
|
175 | commit_hash_prefix = commit_hashes[0] | |
|
176 | ||
|
177 | auth_user = AuthUser( | |
|
178 | user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) | |
|
179 | searcher = searcher_from_config(config) | |
|
180 | result = searcher.search( | |
|
181 | 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user) | |
|
182 | ||
|
183 | return [ | |
|
184 | { | |
|
185 | 'id': entry['commit_id'], | |
|
186 | 'text': entry['commit_id'], | |
|
187 | 'type': 'commit', | |
|
188 | 'obj': {'repo': entry['repository']}, | |
|
189 | 'url': url('changeset_home', | |
|
190 | repo_name=entry['repository'], revision=entry['commit_id']) | |
|
191 | } | |
|
192 | for entry in result['results']] | |
|
193 | ||
|
163 | 194 | @LoginRequired() |
|
164 | 195 | @XHRRequired() |
|
165 | 196 | @jsonify |
|
166 |
def |
|
|
197 | def goto_switcher_data(self): | |
|
167 | 198 | query = request.GET.get('query') |
|
168 |
log.debug('generating switcher |
|
|
199 | log.debug('generating goto switcher list, query %s', query) | |
|
169 | 200 | |
|
170 | 201 | res = [] |
|
171 | 202 | repo_groups = self._get_repo_group_list(query) |
@@ -182,6 +213,19 b' class HomeController(BaseController):' | |||
|
182 | 213 | 'children': repos |
|
183 | 214 | }) |
|
184 | 215 | |
|
216 | commits = self._get_hash_commit_list(query) | |
|
217 | if commits: | |
|
218 | unique_repos = {} | |
|
219 | for commit in commits: | |
|
220 | unique_repos.setdefault(commit['obj']['repo'], [] | |
|
221 | ).append(commit) | |
|
222 | ||
|
223 | for repo in unique_repos: | |
|
224 | res.append({ | |
|
225 | 'text': _('Commits in %(repo)s') % {'repo': repo}, | |
|
226 | 'children': unique_repos[repo] | |
|
227 | }) | |
|
228 | ||
|
185 | 229 | data = { |
|
186 | 230 | 'more': False, |
|
187 | 231 | 'results': res |
@@ -203,6 +247,7 b' class HomeController(BaseController):' | |||
|
203 | 247 | 'text': _('Repositories'), |
|
204 | 248 | 'children': repos |
|
205 | 249 | }) |
|
250 | ||
|
206 | 251 | data = { |
|
207 | 252 | 'more': False, |
|
208 | 253 | 'results': res |
@@ -590,6 +590,8 b' class PullrequestsController(BaseRepoCon' | |||
|
590 | 590 | PullRequestModel().close_pull_request( |
|
591 | 591 | pull_request.pull_request_id, user) |
|
592 | 592 | Session().commit() |
|
593 | msg = _('Pull request was successfully merged and closed.') | |
|
594 | h.flash(msg, category='success') | |
|
593 | 595 | else: |
|
594 | 596 | log.debug( |
|
595 | 597 | "The merge was not successful. Merge response: %s", |
@@ -56,30 +56,33 b' class SearchController(BaseRepoControlle' | |||
|
56 | 56 | search_params = schema.deserialize( |
|
57 | 57 | dict(search_query=request.GET.get('q'), |
|
58 | 58 | search_type=request.GET.get('type'), |
|
59 | search_sort=request.GET.get('sort'), | |
|
59 | 60 | page_limit=request.GET.get('page_limit'), |
|
60 | 61 | requested_page=request.GET.get('page')) |
|
61 | 62 | ) |
|
62 | 63 | except validation_schema.Invalid as e: |
|
63 | 64 | errors = e.children |
|
64 | 65 | |
|
66 | def url_generator(**kw): | |
|
67 | q = urllib.quote(safe_str(search_query)) | |
|
68 | return update_params( | |
|
69 | "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) | |
|
70 | ||
|
65 | 71 | search_query = search_params.get('search_query') |
|
66 | 72 | search_type = search_params.get('search_type') |
|
67 | ||
|
73 | search_sort = search_params.get('search_sort') | |
|
68 | 74 | if search_params.get('search_query'): |
|
69 | 75 | page_limit = search_params['page_limit'] |
|
70 | 76 | requested_page = search_params['requested_page'] |
|
71 | 77 | |
|
72 | def url_generator(**kw): | |
|
73 | q = urllib.quote(safe_str(search_query)) | |
|
74 | return update_params( | |
|
75 | "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) | |
|
76 | 78 | |
|
77 | 79 | c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id, |
|
78 | 80 | ip_addr=self.ip_addr) |
|
79 | 81 | |
|
80 | 82 | try: |
|
81 | 83 | search_result = searcher.search( |
|
82 |
search_query, search_type, c.perm_user, repo_name |
|
|
84 | search_query, search_type, c.perm_user, repo_name, | |
|
85 | requested_page, page_limit, search_sort) | |
|
83 | 86 | |
|
84 | 87 | formatted_results = Page( |
|
85 | 88 | search_result['results'], page=requested_page, |
@@ -97,6 +100,8 b' class SearchController(BaseRepoControlle' | |||
|
97 | 100 | errors = [ |
|
98 | 101 | validation_schema.Invalid(node, search_result['error'])] |
|
99 | 102 | |
|
103 | c.sort = search_sort | |
|
104 | c.url_generator = url_generator | |
|
100 | 105 | c.errors = errors |
|
101 | 106 | c.formatted_results = formatted_results |
|
102 | 107 | c.runtime = execution_time |
@@ -299,6 +299,54 b' def _cached_perms_data(user_id, scope, u' | |||
|
299 | 299 | explicit, algo) |
|
300 | 300 | return permissions.calculate() |
|
301 | 301 | |
|
302 | class PermOrigin: | |
|
303 | ADMIN = 'superadmin' | |
|
304 | ||
|
305 | REPO_USER = 'user:%s' | |
|
306 | REPO_USERGROUP = 'usergroup:%s' | |
|
307 | REPO_OWNER = 'repo.owner' | |
|
308 | REPO_DEFAULT = 'repo.default' | |
|
309 | REPO_PRIVATE = 'repo.private' | |
|
310 | ||
|
311 | REPOGROUP_USER = 'user:%s' | |
|
312 | REPOGROUP_USERGROUP = 'usergroup:%s' | |
|
313 | REPOGROUP_OWNER = 'group.owner' | |
|
314 | REPOGROUP_DEFAULT = 'group.default' | |
|
315 | ||
|
316 | USERGROUP_USER = 'user:%s' | |
|
317 | USERGROUP_USERGROUP = 'usergroup:%s' | |
|
318 | USERGROUP_OWNER = 'usergroup.owner' | |
|
319 | USERGROUP_DEFAULT = 'usergroup.default' | |
|
320 | ||
|
321 | ||
|
322 | class PermOriginDict(dict): | |
|
323 | """ | |
|
324 | A special dict used for tracking permissions along with their origins. | |
|
325 | ||
|
326 | `__setitem__` has been overridden to expect a tuple(perm, origin) | |
|
327 | `__getitem__` will return only the perm | |
|
328 | `.perm_origin_stack` will return the stack of (perm, origin) set per key | |
|
329 | ||
|
330 | >>> perms = PermOriginDict() | |
|
331 | >>> perms['resource'] = 'read', 'default' | |
|
332 | >>> perms['resource'] | |
|
333 | 'read' | |
|
334 | >>> perms['resource'] = 'write', 'admin' | |
|
335 | >>> perms['resource'] | |
|
336 | 'write' | |
|
337 | >>> perms.perm_origin_stack | |
|
338 | {'resource': [('read', 'default'), ('write', 'admin')]} | |
|
339 | """ | |
|
340 | ||
|
341 | ||
|
342 | def __init__(self, *args, **kw): | |
|
343 | dict.__init__(self, *args, **kw) | |
|
344 | self.perm_origin_stack = {} | |
|
345 | ||
|
346 | def __setitem__(self, key, (perm, origin)): | |
|
347 | self.perm_origin_stack.setdefault(key, []).append((perm, origin)) | |
|
348 | dict.__setitem__(self, key, perm) | |
|
349 | ||
|
302 | 350 | |
|
303 | 351 | class PermissionCalculator(object): |
|
304 | 352 | |
@@ -318,9 +366,9 b' class PermissionCalculator(object):' | |||
|
318 | 366 | |
|
319 | 367 | self.default_user_id = User.get_default_user(cache=True).user_id |
|
320 | 368 | |
|
321 |
self.permissions_repositories = |
|
|
322 |
self.permissions_repository_groups = |
|
|
323 |
self.permissions_user_groups = |
|
|
369 | self.permissions_repositories = PermOriginDict() | |
|
370 | self.permissions_repository_groups = PermOriginDict() | |
|
371 | self.permissions_user_groups = PermOriginDict() | |
|
324 | 372 | self.permissions_global = set() |
|
325 | 373 | |
|
326 | 374 | self.default_repo_perms = Permission.get_default_repo_perms( |
@@ -355,19 +403,19 b' class PermissionCalculator(object):' | |||
|
355 | 403 | for perm in self.default_repo_perms: |
|
356 | 404 | r_k = perm.UserRepoToPerm.repository.repo_name |
|
357 | 405 | p = 'repository.admin' |
|
358 | self.permissions_repositories[r_k] = p | |
|
406 | self.permissions_repositories[r_k] = p, PermOrigin.ADMIN | |
|
359 | 407 | |
|
360 | 408 | # repository groups |
|
361 | 409 | for perm in self.default_repo_groups_perms: |
|
362 | 410 | rg_k = perm.UserRepoGroupToPerm.group.group_name |
|
363 | 411 | p = 'group.admin' |
|
364 | self.permissions_repository_groups[rg_k] = p | |
|
412 | self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN | |
|
365 | 413 | |
|
366 | 414 | # user groups |
|
367 | 415 | for perm in self.default_user_group_perms: |
|
368 | 416 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name |
|
369 | 417 | p = 'usergroup.admin' |
|
370 | self.permissions_user_groups[u_k] = p | |
|
418 | self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN | |
|
371 | 419 | |
|
372 | 420 | return self._permission_structure() |
|
373 | 421 | |
@@ -438,8 +486,7 b' class PermissionCalculator(object):' | |||
|
438 | 486 | self.permissions_global = self.permissions_global.difference( |
|
439 | 487 | _configurable) |
|
440 | 488 | for perm in perms: |
|
441 | self.permissions_global.add( | |
|
442 | perm.permission.permission_name) | |
|
489 | self.permissions_global.add(perm.permission.permission_name) | |
|
443 | 490 | |
|
444 | 491 | # user explicit global permissions |
|
445 | 492 | user_perms = Session().query(UserToPerm)\ |
@@ -478,13 +525,16 b' class PermissionCalculator(object):' | |||
|
478 | 525 | # on given repo |
|
479 | 526 | for perm in self.default_repo_perms: |
|
480 | 527 | r_k = perm.UserRepoToPerm.repository.repo_name |
|
528 | o = PermOrigin.REPO_DEFAULT | |
|
481 | 529 | if perm.Repository.private and not ( |
|
482 | 530 | perm.Repository.user_id == self.user_id): |
|
483 | 531 | # disable defaults for private repos, |
|
484 | 532 | p = 'repository.none' |
|
533 | o = PermOrigin.REPO_PRIVATE | |
|
485 | 534 | elif perm.Repository.user_id == self.user_id: |
|
486 | 535 | # set admin if owner |
|
487 | 536 | p = 'repository.admin' |
|
537 | o = PermOrigin.REPO_OWNER | |
|
488 | 538 | else: |
|
489 | 539 | p = perm.Permission.permission_name |
|
490 | 540 | # if we decide this user isn't inheriting permissions from |
@@ -492,15 +542,17 b' class PermissionCalculator(object):' | |||
|
492 | 542 | # permissions work |
|
493 | 543 | if not user_inherit_object_permissions: |
|
494 | 544 | p = 'repository.none' |
|
495 | self.permissions_repositories[r_k] = p | |
|
545 | self.permissions_repositories[r_k] = p, o | |
|
496 | 546 | |
|
497 | 547 | # defaults for repository groups taken from `default` user permission |
|
498 | 548 | # on given group |
|
499 | 549 | for perm in self.default_repo_groups_perms: |
|
500 | 550 | rg_k = perm.UserRepoGroupToPerm.group.group_name |
|
551 | o = PermOrigin.REPOGROUP_DEFAULT | |
|
501 | 552 | if perm.RepoGroup.user_id == self.user_id: |
|
502 | 553 | # set admin if owner |
|
503 | 554 | p = 'group.admin' |
|
555 | o = PermOrigin.REPOGROUP_OWNER | |
|
504 | 556 | else: |
|
505 | 557 | p = perm.Permission.permission_name |
|
506 | 558 | |
@@ -508,18 +560,19 b' class PermissionCalculator(object):' | |||
|
508 | 560 | # user we set him to .none so only explicit permissions work |
|
509 | 561 | if not user_inherit_object_permissions: |
|
510 | 562 | p = 'group.none' |
|
511 | self.permissions_repository_groups[rg_k] = p | |
|
563 | self.permissions_repository_groups[rg_k] = p, o | |
|
512 | 564 | |
|
513 | 565 | # defaults for user groups taken from `default` user permission |
|
514 | 566 | # on given user group |
|
515 | 567 | for perm in self.default_user_group_perms: |
|
516 | 568 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name |
|
517 | 569 | p = perm.Permission.permission_name |
|
570 | o = PermOrigin.USERGROUP_DEFAULT | |
|
518 | 571 | # if we decide this user isn't inheriting permissions from default |
|
519 | 572 | # user we set him to .none so only explicit permissions work |
|
520 | 573 | if not user_inherit_object_permissions: |
|
521 | 574 | p = 'usergroup.none' |
|
522 | self.permissions_user_groups[u_k] = p | |
|
575 | self.permissions_user_groups[u_k] = p, o | |
|
523 | 576 | |
|
524 | 577 | def _calculate_repository_permissions(self): |
|
525 | 578 | """ |
@@ -538,17 +591,20 b' class PermissionCalculator(object):' | |||
|
538 | 591 | multiple_counter = collections.defaultdict(int) |
|
539 | 592 | for perm in user_repo_perms_from_user_group: |
|
540 | 593 | r_k = perm.UserGroupRepoToPerm.repository.repo_name |
|
594 | ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name | |
|
541 | 595 | multiple_counter[r_k] += 1 |
|
542 | 596 | p = perm.Permission.permission_name |
|
597 | o = PermOrigin.REPO_USERGROUP % ug_k | |
|
543 | 598 | |
|
544 | 599 | if perm.Repository.user_id == self.user_id: |
|
545 | 600 | # set admin if owner |
|
546 | 601 | p = 'repository.admin' |
|
602 | o = PermOrigin.REPO_OWNER | |
|
547 | 603 | else: |
|
548 | 604 | if multiple_counter[r_k] > 1: |
|
549 | 605 | cur_perm = self.permissions_repositories[r_k] |
|
550 | 606 | p = self._choose_permission(p, cur_perm) |
|
551 | self.permissions_repositories[r_k] = p | |
|
607 | self.permissions_repositories[r_k] = p, o | |
|
552 | 608 | |
|
553 | 609 | # user explicit permissions for repositories, overrides any specified |
|
554 | 610 | # by the group permission |
@@ -556,16 +612,18 b' class PermissionCalculator(object):' | |||
|
556 | 612 | self.user_id, self.scope_repo_id) |
|
557 | 613 | for perm in user_repo_perms: |
|
558 | 614 | r_k = perm.UserRepoToPerm.repository.repo_name |
|
615 | o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username | |
|
559 | 616 | # set admin if owner |
|
560 | 617 | if perm.Repository.user_id == self.user_id: |
|
561 | 618 | p = 'repository.admin' |
|
619 | o = PermOrigin.REPO_OWNER | |
|
562 | 620 | else: |
|
563 | 621 | p = perm.Permission.permission_name |
|
564 | 622 | if not self.explicit: |
|
565 | 623 | cur_perm = self.permissions_repositories.get( |
|
566 | 624 | r_k, 'repository.none') |
|
567 | 625 | p = self._choose_permission(p, cur_perm) |
|
568 | self.permissions_repositories[r_k] = p | |
|
626 | self.permissions_repositories[r_k] = p, o | |
|
569 | 627 | |
|
570 | 628 | def _calculate_repository_group_permissions(self): |
|
571 | 629 | """ |
@@ -583,32 +641,39 b' class PermissionCalculator(object):' | |||
|
583 | 641 | multiple_counter = collections.defaultdict(int) |
|
584 | 642 | for perm in user_repo_group_perms_from_user_group: |
|
585 | 643 | g_k = perm.UserGroupRepoGroupToPerm.group.group_name |
|
644 | ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name | |
|
645 | o = PermOrigin.REPOGROUP_USERGROUP % ug_k | |
|
586 | 646 | multiple_counter[g_k] += 1 |
|
587 | 647 | p = perm.Permission.permission_name |
|
588 | 648 | if perm.RepoGroup.user_id == self.user_id: |
|
589 | 649 | # set admin if owner |
|
590 | 650 | p = 'group.admin' |
|
651 | o = PermOrigin.REPOGROUP_OWNER | |
|
591 | 652 | else: |
|
592 | 653 | if multiple_counter[g_k] > 1: |
|
593 | 654 | cur_perm = self.permissions_repository_groups[g_k] |
|
594 | 655 | p = self._choose_permission(p, cur_perm) |
|
595 | self.permissions_repository_groups[g_k] = p | |
|
656 | self.permissions_repository_groups[g_k] = p, o | |
|
596 | 657 | |
|
597 | 658 | # user explicit permissions for repository groups |
|
598 | 659 | user_repo_groups_perms = Permission.get_default_group_perms( |
|
599 | 660 | self.user_id, self.scope_repo_group_id) |
|
600 | 661 | for perm in user_repo_groups_perms: |
|
601 | 662 | rg_k = perm.UserRepoGroupToPerm.group.group_name |
|
663 | u_k = perm.UserRepoGroupToPerm.user.username | |
|
664 | o = PermOrigin.REPOGROUP_USER % u_k | |
|
665 | ||
|
602 | 666 | if perm.RepoGroup.user_id == self.user_id: |
|
603 | 667 | # set admin if owner |
|
604 | 668 | p = 'group.admin' |
|
669 | o = PermOrigin.REPOGROUP_OWNER | |
|
605 | 670 | else: |
|
606 | 671 | p = perm.Permission.permission_name |
|
607 | 672 | if not self.explicit: |
|
608 | 673 | cur_perm = self.permissions_repository_groups.get( |
|
609 | 674 | rg_k, 'group.none') |
|
610 | 675 | p = self._choose_permission(p, cur_perm) |
|
611 | self.permissions_repository_groups[rg_k] = p | |
|
676 | self.permissions_repository_groups[rg_k] = p, o | |
|
612 | 677 | |
|
613 | 678 | def _calculate_user_group_permissions(self): |
|
614 | 679 | """ |
@@ -623,24 +688,29 b' class PermissionCalculator(object):' | |||
|
623 | 688 | for perm in user_group_from_user_group: |
|
624 | 689 | g_k = perm.UserGroupUserGroupToPerm\ |
|
625 | 690 | .target_user_group.users_group_name |
|
691 | u_k = perm.UserGroupUserGroupToPerm\ | |
|
692 | .user_group.users_group_name | |
|
693 | o = PermOrigin.USERGROUP_USERGROUP % u_k | |
|
626 | 694 | multiple_counter[g_k] += 1 |
|
627 | 695 | p = perm.Permission.permission_name |
|
628 | 696 | if multiple_counter[g_k] > 1: |
|
629 | 697 | cur_perm = self.permissions_user_groups[g_k] |
|
630 | 698 | p = self._choose_permission(p, cur_perm) |
|
631 | self.permissions_user_groups[g_k] = p | |
|
699 | self.permissions_user_groups[g_k] = p, o | |
|
632 | 700 | |
|
633 | 701 | # user explicit permission for user groups |
|
634 | 702 | user_user_groups_perms = Permission.get_default_user_group_perms( |
|
635 | 703 | self.user_id, self.scope_user_group_id) |
|
636 | 704 | for perm in user_user_groups_perms: |
|
637 | u_k = perm.UserUserGroupToPerm.user_group.users_group_name | |
|
705 | ug_k = perm.UserUserGroupToPerm.user_group.users_group_name | |
|
706 | u_k = perm.UserUserGroupToPerm.user.username | |
|
638 | 707 | p = perm.Permission.permission_name |
|
708 | o = PermOrigin.USERGROUP_USER % u_k | |
|
639 | 709 | if not self.explicit: |
|
640 | 710 | cur_perm = self.permissions_user_groups.get( |
|
641 | u_k, 'usergroup.none') | |
|
711 | ug_k, 'usergroup.none') | |
|
642 | 712 | p = self._choose_permission(p, cur_perm) |
|
643 | self.permissions_user_groups[u_k] = p | |
|
713 | self.permissions_user_groups[ug_k] = p, o | |
|
644 | 714 | |
|
645 | 715 | def _choose_permission(self, new_perm, cur_perm): |
|
646 | 716 | new_perm_val = Permission.PERM_WEIGHTS[new_perm] |
@@ -865,6 +935,10 b' class AuthUser(object):' | |||
|
865 | 935 | return auth_tokens |
|
866 | 936 | |
|
867 | 937 | @property |
|
938 | def is_default(self): | |
|
939 | return self.username == User.DEFAULT_USER | |
|
940 | ||
|
941 | @property | |
|
868 | 942 | def is_admin(self): |
|
869 | 943 | return self.admin |
|
870 | 944 | |
@@ -1095,6 +1169,7 b' class LoginRequired(object):' | |||
|
1095 | 1169 | return get_cython_compat_decorator(self.__wrapper, func) |
|
1096 | 1170 | |
|
1097 | 1171 | def __wrapper(self, func, *fargs, **fkwargs): |
|
1172 | from rhodecode.lib import helpers as h | |
|
1098 | 1173 | cls = fargs[0] |
|
1099 | 1174 | user = cls._rhodecode_user |
|
1100 | 1175 | loc = "%s:%s" % (cls.__class__.__name__, func.__name__) |
@@ -1102,7 +1177,6 b' class LoginRequired(object):' | |||
|
1102 | 1177 | # check if our IP is allowed |
|
1103 | 1178 | ip_access_valid = True |
|
1104 | 1179 | if not user.ip_allowed: |
|
1105 | from rhodecode.lib import helpers as h | |
|
1106 | 1180 | h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))), |
|
1107 | 1181 | category='warning') |
|
1108 | 1182 | ip_access_valid = False |
@@ -1154,7 +1228,7 b' class LoginRequired(object):' | |||
|
1154 | 1228 | |
|
1155 | 1229 | log.debug('redirecting to login page with %s' % (came_from,)) |
|
1156 | 1230 | return redirect( |
|
1157 |
|
|
|
1231 | h.route_path('login', _query={'came_from': came_from})) | |
|
1158 | 1232 | |
|
1159 | 1233 | |
|
1160 | 1234 | class NotAnonymous(object): |
@@ -1180,7 +1254,8 b' class NotAnonymous(object):' | |||
|
1180 | 1254 | h.flash(_('You need to be a registered user to ' |
|
1181 | 1255 | 'perform this action'), |
|
1182 | 1256 | category='warning') |
|
1183 | return redirect(url('login_home', came_from=came_from)) | |
|
1257 | return redirect( | |
|
1258 | h.route_path('login', _query={'came_from': came_from})) | |
|
1184 | 1259 | else: |
|
1185 | 1260 | return func(*fargs, **fkwargs) |
|
1186 | 1261 | |
@@ -1263,7 +1338,8 b' class PermsDecorator(object):' | |||
|
1263 | 1338 | import rhodecode.lib.helpers as h |
|
1264 | 1339 | h.flash(_('You need to be signed in to view this page'), |
|
1265 | 1340 | category='warning') |
|
1266 |
return redirect( |
|
|
1341 | return redirect( | |
|
1342 | h.route_path('login', _query={'came_from': came_from})) | |
|
1267 | 1343 | |
|
1268 | 1344 | else: |
|
1269 | 1345 | # redirect with forbidden ret code |
@@ -35,7 +35,7 b' def makedate():' | |||
|
35 | 35 | return time.mktime(lt), tz |
|
36 | 36 | |
|
37 | 37 | |
|
38 | def date_fromtimestamp(unixts, tzoffset=0): | |
|
38 | def utcdate_fromtimestamp(unixts, tzoffset=0): | |
|
39 | 39 | """ |
|
40 | 40 | Makes a local datetime object out of unix timestamp |
|
41 | 41 | |
@@ -43,7 +43,7 b' def date_fromtimestamp(unixts, tzoffset=' | |||
|
43 | 43 | :param tzoffset: |
|
44 | 44 | """ |
|
45 | 45 | |
|
46 | return datetime.datetime.fromtimestamp(float(unixts)) | |
|
46 | return datetime.datetime.utcfromtimestamp(float(unixts)) | |
|
47 | 47 | |
|
48 | 48 | |
|
49 | 49 | def date_astimestamp(value): |
@@ -537,7 +537,6 b' class DbManage(object):' | |||
|
537 | 537 | ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'), |
|
538 | 538 | ('support_url', '', 'unicode'), |
|
539 | 539 | ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'), |
|
540 | ('license_key', '', 'unicode'), | |
|
541 | 540 | ('show_revision_number', True, 'bool'), |
|
542 | 541 | ('show_sha_length', 12, 'int'), |
|
543 | 542 | ] |
@@ -36,11 +36,14 b' import urlparse' | |||
|
36 | 36 | import time |
|
37 | 37 | import string |
|
38 | 38 | import hashlib |
|
39 | import pygments | |
|
39 | 40 | |
|
40 | 41 | from datetime import datetime |
|
41 | 42 | from functools import partial |
|
42 | 43 | from pygments.formatters.html import HtmlFormatter |
|
43 | 44 | from pygments import highlight as code_highlight |
|
45 | from pygments.lexers import ( | |
|
46 | get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype) | |
|
44 | 47 | from pylons import url |
|
45 | 48 | from pylons.i18n.translation import _, ungettext |
|
46 | 49 | from pyramid.threadlocal import get_current_request |
@@ -68,8 +71,8 b' from rhodecode.lib.annotate import annot' | |||
|
68 | 71 | from rhodecode.lib.action_parser import action_parser |
|
69 | 72 | from rhodecode.lib.utils import repo_name_slug, get_custom_lexer |
|
70 | 73 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ |
|
71 |
get_commit_safe, datetime_to_time, time_to_datetime, |
|
|
72 | safe_int, md5, md5_safe | |
|
74 | get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \ | |
|
75 | AttributeDict, safe_int, md5, md5_safe | |
|
73 | 76 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
74 | 77 | from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError |
|
75 | 78 | from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit |
@@ -307,6 +310,176 b' class CodeHtmlFormatter(HtmlFormatter):' | |||
|
307 | 310 | yield 0, '</td></tr></table>' |
|
308 | 311 | |
|
309 | 312 | |
|
313 | class SearchContentCodeHtmlFormatter(CodeHtmlFormatter): | |
|
314 | def __init__(self, **kw): | |
|
315 | # only show these line numbers if set | |
|
316 | self.only_lines = kw.pop('only_line_numbers', []) | |
|
317 | self.query_terms = kw.pop('query_terms', []) | |
|
318 | self.max_lines = kw.pop('max_lines', 5) | |
|
319 | self.line_context = kw.pop('line_context', 3) | |
|
320 | self.url = kw.pop('url', None) | |
|
321 | ||
|
322 | super(CodeHtmlFormatter, self).__init__(**kw) | |
|
323 | ||
|
324 | def _wrap_code(self, source): | |
|
325 | for cnt, it in enumerate(source): | |
|
326 | i, t = it | |
|
327 | t = '<pre>%s</pre>' % t | |
|
328 | yield i, t | |
|
329 | ||
|
330 | def _wrap_tablelinenos(self, inner): | |
|
331 | yield 0, '<table class="code-highlight %stable">' % self.cssclass | |
|
332 | ||
|
333 | last_shown_line_number = 0 | |
|
334 | current_line_number = 1 | |
|
335 | ||
|
336 | for t, line in inner: | |
|
337 | if not t: | |
|
338 | yield t, line | |
|
339 | continue | |
|
340 | ||
|
341 | if current_line_number in self.only_lines: | |
|
342 | if last_shown_line_number + 1 != current_line_number: | |
|
343 | yield 0, '<tr>' | |
|
344 | yield 0, '<td class="line">...</td>' | |
|
345 | yield 0, '<td id="hlcode" class="code"></td>' | |
|
346 | yield 0, '</tr>' | |
|
347 | ||
|
348 | yield 0, '<tr>' | |
|
349 | if self.url: | |
|
350 | yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % ( | |
|
351 | self.url, current_line_number, current_line_number) | |
|
352 | else: | |
|
353 | yield 0, '<td class="line"><a href="">%i</a></td>' % ( | |
|
354 | current_line_number) | |
|
355 | yield 0, '<td id="hlcode" class="code">' + line + '</td>' | |
|
356 | yield 0, '</tr>' | |
|
357 | ||
|
358 | last_shown_line_number = current_line_number | |
|
359 | ||
|
360 | current_line_number += 1 | |
|
361 | ||
|
362 | ||
|
363 | yield 0, '</table>' | |
|
364 | ||
|
365 | ||
|
366 | def extract_phrases(text_query): | |
|
367 | """ | |
|
368 | Extracts phrases from search term string making sure phrases | |
|
369 | contained in double quotes are kept together - and discarding empty values | |
|
370 | or fully whitespace values eg. | |
|
371 | ||
|
372 | 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more'] | |
|
373 | ||
|
374 | """ | |
|
375 | ||
|
376 | in_phrase = False | |
|
377 | buf = '' | |
|
378 | phrases = [] | |
|
379 | for char in text_query: | |
|
380 | if in_phrase: | |
|
381 | if char == '"': # end phrase | |
|
382 | phrases.append(buf) | |
|
383 | buf = '' | |
|
384 | in_phrase = False | |
|
385 | continue | |
|
386 | else: | |
|
387 | buf += char | |
|
388 | continue | |
|
389 | else: | |
|
390 | if char == '"': # start phrase | |
|
391 | in_phrase = True | |
|
392 | phrases.append(buf) | |
|
393 | buf = '' | |
|
394 | continue | |
|
395 | elif char == ' ': | |
|
396 | phrases.append(buf) | |
|
397 | buf = '' | |
|
398 | continue | |
|
399 | else: | |
|
400 | buf += char | |
|
401 | ||
|
402 | phrases.append(buf) | |
|
403 | phrases = [phrase.strip() for phrase in phrases if phrase.strip()] | |
|
404 | return phrases | |
|
405 | ||
|
406 | ||
|
407 | def get_matching_offsets(text, phrases): | |
|
408 | """ | |
|
409 | Returns a list of string offsets in `text` that the list of `terms` match | |
|
410 | ||
|
411 | >>> get_matching_offsets('some text here', ['some', 'here']) | |
|
412 | [(0, 4), (10, 14)] | |
|
413 | ||
|
414 | """ | |
|
415 | offsets = [] | |
|
416 | for phrase in phrases: | |
|
417 | for match in re.finditer(phrase, text): | |
|
418 | offsets.append((match.start(), match.end())) | |
|
419 | ||
|
420 | return offsets | |
|
421 | ||
|
422 | ||
|
423 | def normalize_text_for_matching(x): | |
|
424 | """ | |
|
425 | Replaces all non alnum characters to spaces and lower cases the string, | |
|
426 | useful for comparing two text strings without punctuation | |
|
427 | """ | |
|
428 | return re.sub(r'[^\w]', ' ', x.lower()) | |
|
429 | ||
|
430 | ||
|
431 | def get_matching_line_offsets(lines, terms): | |
|
432 | """ Return a set of `lines` indices (starting from 1) matching a | |
|
433 | text search query, along with `context` lines above/below matching lines | |
|
434 | ||
|
435 | :param lines: list of strings representing lines | |
|
436 | :param terms: search term string to match in lines eg. 'some text' | |
|
437 | :param context: number of lines above/below a matching line to add to result | |
|
438 | :param max_lines: cut off for lines of interest | |
|
439 | eg. | |
|
440 | ||
|
441 | >>> get_matching_line_offsets(''' | |
|
442 | words words words | |
|
443 | words words words | |
|
444 | some text some | |
|
445 | words words words | |
|
446 | words words words | |
|
447 | text here what | |
|
448 | ''', 'text', context=1) | |
|
449 | {3: [(5, 9)], 6: [(0, 4)]] | |
|
450 | """ | |
|
451 | matching_lines = {} | |
|
452 | phrases = [normalize_text_for_matching(phrase) | |
|
453 | for phrase in extract_phrases(terms)] | |
|
454 | ||
|
455 | for line_index, line in enumerate(lines, start=1): | |
|
456 | match_offsets = get_matching_offsets( | |
|
457 | normalize_text_for_matching(line), phrases) | |
|
458 | if match_offsets: | |
|
459 | matching_lines[line_index] = match_offsets | |
|
460 | ||
|
461 | return matching_lines | |
|
462 | ||
|
463 | def get_lexer_safe(mimetype=None, filepath=None): | |
|
464 | """ | |
|
465 | Tries to return a relevant pygments lexer using mimetype/filepath name, | |
|
466 | defaulting to plain text if none could be found | |
|
467 | """ | |
|
468 | lexer = None | |
|
469 | try: | |
|
470 | if mimetype: | |
|
471 | lexer = get_lexer_for_mimetype(mimetype) | |
|
472 | if not lexer: | |
|
473 | lexer = get_lexer_for_filename(path) | |
|
474 | except pygments.util.ClassNotFound: | |
|
475 | pass | |
|
476 | ||
|
477 | if not lexer: | |
|
478 | lexer = get_lexer_by_name('text') | |
|
479 | ||
|
480 | return lexer | |
|
481 | ||
|
482 | ||
|
310 | 483 | def pygmentize(filenode, **kwargs): |
|
311 | 484 | """ |
|
312 | 485 | pygmentize function using pygments |
@@ -476,13 +649,20 b' short_id = lambda x: x[:12]' | |||
|
476 | 649 | hide_credentials = lambda x: ''.join(credentials_filter(x)) |
|
477 | 650 | |
|
478 | 651 | |
|
479 | def age_component(datetime_iso, value=None): | |
|
652 | def age_component(datetime_iso, value=None, time_is_local=False): | |
|
480 | 653 | title = value or format_date(datetime_iso) |
|
481 | 654 | |
|
482 |
# detect if we have a timezone info, |
|
|
655 | # detect if we have a timezone info, otherwise, add it | |
|
483 | 656 | if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo: |
|
484 | 657 | tzinfo = '+00:00' |
|
485 | 658 | |
|
659 | if time_is_local: | |
|
660 | tzinfo = time.strftime("+%H:%M", | |
|
661 | time.gmtime( | |
|
662 | (datetime.now() - datetime.utcnow()).seconds + 1 | |
|
663 | ) | |
|
664 | ) | |
|
665 | ||
|
486 | 666 | return literal( |
|
487 | 667 | '<time class="timeago tooltip" ' |
|
488 | 668 | 'title="{1}" datetime="{0}{2}">{1}</time>'.format( |
@@ -42,7 +42,6 b' class BaseSearch(object):' | |||
|
42 | 42 | def search(self, query, document_type, search_user, repo_name=None): |
|
43 | 43 | raise Exception('NotImplemented') |
|
44 | 44 | |
|
45 | ||
|
46 | 45 | def searcher_from_config(config, prefix='search.'): |
|
47 | 46 | _config = {} |
|
48 | 47 | for key in config.keys(): |
@@ -25,6 +25,7 b' Index schema for RhodeCode' | |||
|
25 | 25 | from __future__ import absolute_import |
|
26 | 26 | import logging |
|
27 | 27 | import os |
|
28 | import re | |
|
28 | 29 | |
|
29 | 30 | from pylons.i18n.translation import _ |
|
30 | 31 | |
@@ -59,6 +60,7 b' FRAGMENTER = ContextFragmenter(200)' | |||
|
59 | 60 | log = logging.getLogger(__name__) |
|
60 | 61 | |
|
61 | 62 | |
|
63 | ||
|
62 | 64 | class Search(BaseSearch): |
|
63 | 65 | |
|
64 | 66 | name = 'whoosh' |
@@ -90,7 +92,19 b' class Search(BaseSearch):' | |||
|
90 | 92 | if self.searcher: |
|
91 | 93 | self.searcher.close() |
|
92 | 94 | |
|
93 | def search(self, query, document_type, search_user, repo_name=None): | |
|
95 | def _extend_query(self, query): | |
|
96 | hashes = re.compile('([0-9a-f]{5,40})').findall(query) | |
|
97 | if hashes: | |
|
98 | hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes) | |
|
99 | query = u'(%s) OR %s' % (query, hashes_or_query) | |
|
100 | return query | |
|
101 | ||
|
102 | def search(self, query, document_type, search_user, repo_name=None, | |
|
103 | requested_page=1, page_limit=10, sort=None): | |
|
104 | ||
|
105 | original_query = query | |
|
106 | query = self._extend_query(query) | |
|
107 | ||
|
94 | 108 | log.debug(u'QUERY: %s on %s', query, document_type) |
|
95 | 109 | result = { |
|
96 | 110 | 'results': [], |
@@ -109,13 +123,18 b' class Search(BaseSearch):' | |||
|
109 | 123 | query = qp.parse(unicode(query)) |
|
110 | 124 | log.debug('query: %s (%s)' % (query, repr(query))) |
|
111 | 125 | |
|
112 | sortedby = None | |
|
126 | reverse, sortedby = False, None | |
|
113 | 127 | if search_type == 'message': |
|
114 | sortedby = sorting.FieldFacet('commit_idx', reverse=True) | |
|
128 | if sort == 'oldfirst': | |
|
129 | sortedby = 'date' | |
|
130 | reverse = False | |
|
131 | elif sort == 'newfirst': | |
|
132 | sortedby = 'date' | |
|
133 | reverse = True | |
|
115 | 134 | |
|
116 | 135 | whoosh_results = self.searcher.search( |
|
117 | 136 | query, filter=allowed_repos_filter, limit=None, |
|
118 | sortedby=sortedby,) | |
|
137 | sortedby=sortedby, reverse=reverse) | |
|
119 | 138 | |
|
120 | 139 | # fixes for 32k limit that whoosh uses for highlight |
|
121 | 140 | whoosh_results.fragmenter.charlimit = None |
@@ -63,7 +63,7 b' COMMIT_SCHEMA = Schema(' | |||
|
63 | 63 | repository_id=NUMERIC(unique=True, stored=True), |
|
64 | 64 | commit_idx=NUMERIC(stored=True, sortable=True), |
|
65 | 65 | commit_idx_sort=ID(), |
|
66 | date=NUMERIC(stored=True), | |
|
66 | date=NUMERIC(stored=True, sortable=True), | |
|
67 | 67 | owner=TEXT(stored=True), |
|
68 | 68 | author=TEXT(stored=True), |
|
69 | 69 | message=FieldType(format=Characters(), analyzer=ANALYZER, |
@@ -755,10 +755,10 b' def create_test_env(repos_test_path, con' | |||
|
755 | 755 | # PART TWO make test repo |
|
756 | 756 | log.debug('making test vcs repositories') |
|
757 | 757 | |
|
758 |
idx_path = config[' |
|
|
759 |
data_path = config[' |
|
|
758 | idx_path = config['search.location'] | |
|
759 | data_path = config['cache_dir'] | |
|
760 | 760 | |
|
761 | #clean index and data | |
|
761 | # clean index and data | |
|
762 | 762 | if idx_path and os.path.exists(idx_path): |
|
763 | 763 | log.debug('remove %s', idx_path) |
|
764 | 764 | shutil.rmtree(idx_path) |
@@ -767,7 +767,7 b' def create_test_env(repos_test_path, con' | |||
|
767 | 767 | log.debug('remove %s', data_path) |
|
768 | 768 | shutil.rmtree(data_path) |
|
769 | 769 | |
|
770 | #CREATE DEFAULT TEST REPOS | |
|
770 | # CREATE DEFAULT TEST REPOS | |
|
771 | 771 | cur_dir = dn(dn(abspath(__file__))) |
|
772 | 772 | with tarfile.open(jn(cur_dir, 'tests', 'fixtures', |
|
773 | 773 | 'vcs_test_hg.tar.gz')) as tar: |
@@ -787,7 +787,6 b' def create_test_env(repos_test_path, con' | |||
|
787 | 787 | tar.extractall(jn(TESTS_TMP_PATH, SVN_REPO)) |
|
788 | 788 | |
|
789 | 789 | |
|
790 | ||
|
791 | 790 | #============================================================================== |
|
792 | 791 | # PASTER COMMANDS |
|
793 | 792 | #============================================================================== |
@@ -608,6 +608,16 b' def time_to_datetime(tm):' | |||
|
608 | 608 | return datetime.datetime.fromtimestamp(tm) |
|
609 | 609 | |
|
610 | 610 | |
|
611 | def time_to_utcdatetime(tm): | |
|
612 | if tm: | |
|
613 | if isinstance(tm, basestring): | |
|
614 | try: | |
|
615 | tm = float(tm) | |
|
616 | except ValueError: | |
|
617 | return | |
|
618 | return datetime.datetime.utcfromtimestamp(tm) | |
|
619 | ||
|
620 | ||
|
611 | 621 | MENTIONS_REGEX = re.compile( |
|
612 | 622 | # ^@ or @ without any special chars in front |
|
613 | 623 | r'(?:^@|[^a-zA-Z0-9\-\_\.]@)' |
@@ -409,7 +409,9 b' class BaseRepository(object):' | |||
|
409 | 409 | shadow_repository_path, target_ref, source_repo, |
|
410 | 410 | source_ref, message, user_name, user_email, dry_run=dry_run) |
|
411 | 411 | except RepositoryError: |
|
412 | log.exception('Unexpected failure when running merge') | |
|
412 | log.exception( | |
|
413 | 'Unexpected failure when running merge, dry-run=%s', | |
|
414 | dry_run) | |
|
413 | 415 | return MergeResponse( |
|
414 | 416 | False, False, None, MergeFailureReason.UNKNOWN) |
|
415 | 417 |
@@ -30,7 +30,7 b' from StringIO import StringIO' | |||
|
30 | 30 | |
|
31 | 31 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
32 | 32 | |
|
33 | from rhodecode.lib.datelib import date_fromtimestamp | |
|
33 | from rhodecode.lib.datelib import utcdate_fromtimestamp | |
|
34 | 34 | from rhodecode.lib.utils import safe_unicode, safe_str |
|
35 | 35 | from rhodecode.lib.utils2 import safe_int |
|
36 | 36 | from rhodecode.lib.vcs.conf import settings |
@@ -95,7 +95,7 b' class GitCommit(base.BaseCommit):' | |||
|
95 | 95 | if value: |
|
96 | 96 | value = safe_unicode(value) |
|
97 | 97 | elif attr == "date": |
|
98 | value = date_fromtimestamp(*value) | |
|
98 | value = utcdate_fromtimestamp(*value) | |
|
99 | 99 | elif attr == "parents": |
|
100 | 100 | value = self._make_commits(value) |
|
101 | 101 | self.__dict__[attr] = value |
@@ -135,7 +135,7 b' class GitCommit(base.BaseCommit):' | |||
|
135 | 135 | def date(self): |
|
136 | 136 | unix_ts, tz = self._remote.get_object_attrs( |
|
137 | 137 | self.raw_id, self._date_property, self._date_tz_property) |
|
138 | return date_fromtimestamp(unix_ts, tz) | |
|
138 | return utcdate_fromtimestamp(unix_ts, tz) | |
|
139 | 139 | |
|
140 | 140 | @LazyProperty |
|
141 | 141 | def status(self): |
@@ -31,7 +31,7 b' import time' | |||
|
31 | 31 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
32 | 32 | |
|
33 | 33 | from rhodecode.lib.compat import OrderedDict |
|
34 | from rhodecode.lib.datelib import makedate, date_fromtimestamp | |
|
34 | from rhodecode.lib.datelib import makedate, utcdate_fromtimestamp | |
|
35 | 35 | from rhodecode.lib.utils import safe_unicode, safe_str |
|
36 | 36 | from rhodecode.lib.vcs import connection, path as vcspath |
|
37 | 37 | from rhodecode.lib.vcs.backends.base import ( |
@@ -269,7 +269,7 b' class GitRepository(BaseRepository):' | |||
|
269 | 269 | Returns last change made on this repository as |
|
270 | 270 | `datetime.datetime` object. |
|
271 | 271 | """ |
|
272 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) | |
|
272 | return utcdate_fromtimestamp(self._get_mtime(), makedate()[1]) | |
|
273 | 273 | |
|
274 | 274 | def _get_mtime(self): |
|
275 | 275 | try: |
@@ -853,7 +853,8 b' class GitRepository(BaseRepository):' | |||
|
853 | 853 | shadow_repo._checkout(pr_branch, create=True) |
|
854 | 854 | try: |
|
855 | 855 | shadow_repo._local_fetch(source_repo.path, source_ref.name) |
|
856 | except RepositoryError: | |
|
856 | except RepositoryError as e: | |
|
857 | log.exception('Failure when doing local fetch on git shadow repo') | |
|
857 | 858 | return MergeResponse( |
|
858 | 859 | False, False, None, MergeFailureReason.MISSING_COMMIT) |
|
859 | 860 | |
@@ -863,7 +864,8 b' class GitRepository(BaseRepository):' | |||
|
863 | 864 | shadow_repo._local_merge(merge_message, merger_name, merger_email, |
|
864 | 865 | [source_ref.commit_id]) |
|
865 | 866 | merge_possible = True |
|
866 | except RepositoryError: | |
|
867 | except RepositoryError as e: | |
|
868 | log.exception('Failure when doing local merge on git shadow repo') | |
|
867 | 869 | merge_possible = False |
|
868 | 870 | merge_failure_reason = MergeFailureReason.MERGE_FAILED |
|
869 | 871 | |
@@ -877,7 +879,9 b' class GitRepository(BaseRepository):' | |||
|
877 | 879 | # cannot retrieve the merge commit. |
|
878 | 880 | shadow_repo = GitRepository(shadow_repository_path) |
|
879 | 881 | merge_commit_id = shadow_repo.branches[pr_branch] |
|
880 | except RepositoryError: | |
|
882 | except RepositoryError as e: | |
|
883 | log.exception( | |
|
884 | 'Failure when doing local push on git shadow repo') | |
|
881 | 885 | merge_succeeded = False |
|
882 | 886 | merge_failure_reason = MergeFailureReason.PUSH_FAILED |
|
883 | 887 | else: |
@@ -26,7 +26,7 b' import os' | |||
|
26 | 26 | |
|
27 | 27 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
28 | 28 | |
|
29 | from rhodecode.lib.datelib import date_fromtimestamp | |
|
29 | from rhodecode.lib.datelib import utcdate_fromtimestamp | |
|
30 | 30 | from rhodecode.lib.utils import safe_str, safe_unicode |
|
31 | 31 | from rhodecode.lib.vcs import path as vcspath |
|
32 | 32 | from rhodecode.lib.vcs.backends import base |
@@ -78,7 +78,7 b' class MercurialCommit(base.BaseCommit):' | |||
|
78 | 78 | elif attr == "affected_files": |
|
79 | 79 | value = map(safe_unicode, value) |
|
80 | 80 | elif attr == "date": |
|
81 | value = date_fromtimestamp(*value) | |
|
81 | value = utcdate_fromtimestamp(*value) | |
|
82 | 82 | elif attr in ["children", "parents"]: |
|
83 | 83 | value = self._make_commits(value) |
|
84 | 84 | self.__dict__[attr] = value |
@@ -114,7 +114,7 b' class MercurialCommit(base.BaseCommit):' | |||
|
114 | 114 | |
|
115 | 115 | @LazyProperty |
|
116 | 116 | def date(self): |
|
117 | return date_fromtimestamp(*self._remote.ctx_date(self.idx)) | |
|
117 | return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx)) | |
|
118 | 118 | |
|
119 | 119 | @LazyProperty |
|
120 | 120 | def status(self): |
@@ -22,6 +22,7 b'' | |||
|
22 | 22 | HG repository module |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | import logging | |
|
25 | 26 | import binascii |
|
26 | 27 | import os |
|
27 | 28 | import re |
@@ -31,9 +32,8 b' import urllib' | |||
|
31 | 32 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
32 | 33 | |
|
33 | 34 | from rhodecode.lib.compat import OrderedDict |
|
34 | from rhodecode.lib.datelib import ( | |
|
35 |
date_fromtimestamp, makedate, date_ |
|
|
36 | date_astimestamp) | |
|
35 | from rhodecode.lib.datelib import (date_to_timestamp_plus_offset, | |
|
36 | utcdate_fromtimestamp, makedate, date_astimestamp) | |
|
37 | 37 | from rhodecode.lib.utils import safe_unicode, safe_str |
|
38 | 38 | from rhodecode.lib.vcs import connection |
|
39 | 39 | from rhodecode.lib.vcs.backends.base import ( |
@@ -50,6 +50,8 b' from rhodecode.lib.vcs.exceptions import' | |||
|
50 | 50 | hexlify = binascii.hexlify |
|
51 | 51 | nullid = "\0" * 20 |
|
52 | 52 | |
|
53 | log = logging.getLogger(__name__) | |
|
54 | ||
|
53 | 55 | |
|
54 | 56 | class MercurialRepository(BaseRepository): |
|
55 | 57 | """ |
@@ -365,7 +367,7 b' class MercurialRepository(BaseRepository' | |||
|
365 | 367 | Returns last change made on this repository as |
|
366 | 368 | `datetime.datetime` object |
|
367 | 369 | """ |
|
368 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) | |
|
370 | return utcdate_fromtimestamp(self._get_mtime(), makedate()[1]) | |
|
369 | 371 | |
|
370 | 372 | def _get_mtime(self): |
|
371 | 373 | try: |
@@ -605,6 +607,10 b' class MercurialRepository(BaseRepository' | |||
|
605 | 607 | self._update(bookmark_name) |
|
606 | 608 | return self._identify(), True |
|
607 | 609 | except RepositoryError: |
|
610 | # The rebase-abort may raise another exception which 'hides' | |
|
611 | # the original one, therefore we log it here. | |
|
612 | log.exception('Error while rebasing shadow repo during merge.') | |
|
613 | ||
|
608 | 614 | # Cleanup any rebase leftovers |
|
609 | 615 | self._remote.rebase(abort=True) |
|
610 | 616 | self._remote.update(clean=True) |
@@ -642,6 +648,8 b' class MercurialRepository(BaseRepository' | |||
|
642 | 648 | shadow_repository_path = self._get_shadow_repository_path(workspace_id) |
|
643 | 649 | if not os.path.exists(shadow_repository_path): |
|
644 | 650 | self._local_clone(shadow_repository_path) |
|
651 | log.debug( | |
|
652 | 'Prepared shadow repository in %s', shadow_repository_path) | |
|
645 | 653 | |
|
646 | 654 | return shadow_repository_path |
|
647 | 655 | |
@@ -664,12 +672,15 b' class MercurialRepository(BaseRepository' | |||
|
664 | 672 | |
|
665 | 673 | shadow_repo = self._get_shadow_instance(shadow_repository_path) |
|
666 | 674 | |
|
675 | log.debug('Pulling in target reference %s', target_ref) | |
|
667 | 676 | self._validate_pull_reference(target_ref) |
|
668 | 677 | shadow_repo._local_pull(self.path, target_ref) |
|
669 | 678 | try: |
|
679 | log.debug('Pulling in source reference %s', source_ref) | |
|
670 | 680 | source_repo._validate_pull_reference(source_ref) |
|
671 | 681 | shadow_repo._local_pull(source_repo.path, source_ref) |
|
672 | except CommitDoesNotExistError: | |
|
682 | except CommitDoesNotExistError as e: | |
|
683 | log.exception('Failure when doing local pull on hg shadow repo') | |
|
673 | 684 | return MergeResponse( |
|
674 | 685 | False, False, None, MergeFailureReason.MISSING_COMMIT) |
|
675 | 686 | |
@@ -681,7 +692,8 b' class MercurialRepository(BaseRepository' | |||
|
681 | 692 | target_ref, merge_message, merger_name, merger_email, |
|
682 | 693 | source_ref) |
|
683 | 694 | merge_possible = True |
|
684 | except RepositoryError: | |
|
695 | except RepositoryError as e: | |
|
696 | log.exception('Failure when doing local merge on hg shadow repo') | |
|
685 | 697 | merge_possible = False |
|
686 | 698 | merge_failure_reason = MergeFailureReason.MERGE_FAILED |
|
687 | 699 | |
@@ -706,6 +718,9 b' class MercurialRepository(BaseRepository' | |||
|
706 | 718 | enable_hooks=True) |
|
707 | 719 | merge_succeeded = True |
|
708 | 720 | except RepositoryError: |
|
721 | log.exception( | |
|
722 | 'Failure when doing local push from the shadow ' | |
|
723 | 'repository to the target repository.') | |
|
709 | 724 | merge_succeeded = False |
|
710 | 725 | merge_failure_reason = MergeFailureReason.PUSH_FAILED |
|
711 | 726 | else: |
@@ -1593,7 +1593,7 b' class Repository(Base, BaseModel):' | |||
|
1593 | 1593 | 'repo_id': repo.repo_id, |
|
1594 | 1594 | 'repo_name': repo.repo_name, |
|
1595 | 1595 | 'repo_type': repo.repo_type, |
|
1596 | 'clone_uri': repo.clone_uri, | |
|
1596 | 'clone_uri': repo.clone_uri or '', | |
|
1597 | 1597 | 'private': repo.private, |
|
1598 | 1598 | 'created_on': repo.created_on, |
|
1599 | 1599 | 'description': repo.description, |
@@ -2794,7 +2794,9 b' class CacheKey(Base, BaseModel):' | |||
|
2794 | 2794 | |
|
2795 | 2795 | Session().commit() |
|
2796 | 2796 | except Exception: |
|
2797 | log.error(traceback.format_exc()) | |
|
2797 | log.exception( | |
|
2798 | 'Cache key invalidation failed for repository %s', | |
|
2799 | safe_str(repo_name)) | |
|
2798 | 2800 | Session().rollback() |
|
2799 | 2801 | |
|
2800 | 2802 | @classmethod |
@@ -396,10 +396,15 b' class PullRequestModel(BaseModel):' | |||
|
396 | 396 | return commit_ids |
|
397 | 397 | |
|
398 | 398 | def merge(self, pull_request, user, extras): |
|
399 | log.debug("Merging pull request %s", pull_request.pull_request_id) | |
|
399 | 400 | merge_state = self._merge_pull_request(pull_request, user, extras) |
|
400 | 401 | if merge_state.executed: |
|
402 | log.debug( | |
|
403 | "Merge was successful, updating the pull request comments.") | |
|
401 | 404 | self._comment_and_close_pr(pull_request, user, merge_state) |
|
402 | 405 | self._log_action('user_merged_pull_request', user, pull_request) |
|
406 | else: | |
|
407 | log.warn("Merge failed, not updating the pull request.") | |
|
403 | 408 | return merge_state |
|
404 | 409 | |
|
405 | 410 | def _merge_pull_request(self, pull_request, user, extras): |
@@ -907,15 +912,20 b' class PullRequestModel(BaseModel):' | |||
|
907 | 912 | """ |
|
908 | 913 | Try to merge the pull request and return the merge status. |
|
909 | 914 | """ |
|
915 | log.debug( | |
|
916 | "Trying out if the pull request %s can be merged.", | |
|
917 | pull_request.pull_request_id) | |
|
910 | 918 | target_vcs = pull_request.target_repo.scm_instance() |
|
911 | 919 | target_ref = self._refresh_reference( |
|
912 | 920 | pull_request.target_ref_parts, target_vcs) |
|
913 | 921 | |
|
914 | 922 | target_locked = pull_request.target_repo.locked |
|
915 | 923 | if target_locked and target_locked[0]: |
|
924 | log.debug("The target repository is locked.") | |
|
916 | 925 | merge_state = MergeResponse( |
|
917 | 926 | False, False, None, MergeFailureReason.TARGET_IS_LOCKED) |
|
918 | 927 | elif self._needs_merge_state_refresh(pull_request, target_ref): |
|
928 | log.debug("Refreshing the merge status of the repository.") | |
|
919 | 929 | merge_state = self._refresh_merge_state( |
|
920 | 930 | pull_request, target_vcs, target_ref) |
|
921 | 931 | else: |
@@ -923,6 +933,7 b' class PullRequestModel(BaseModel):' | |||
|
923 | 933 | _last_merge_status == MergeFailureReason.NONE |
|
924 | 934 | merge_state = MergeResponse( |
|
925 | 935 | possible, False, None, pull_request._last_merge_status) |
|
936 | log.debug("Merge response: %s", merge_state) | |
|
926 | 937 | return merge_state |
|
927 | 938 | |
|
928 | 939 | def _refresh_reference(self, reference, vcs_repository): |
@@ -449,7 +449,7 b' class ScmModel(BaseModel):' | |||
|
449 | 449 | return tip |
|
450 | 450 | |
|
451 | 451 | def _sanitize_path(self, f_path): |
|
452 | if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path: | |
|
452 | if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path: | |
|
453 | 453 | raise NonRelativePathError('%s is not an relative path' % f_path) |
|
454 | 454 | if f_path: |
|
455 | 455 | f_path = os.path.normpath(f_path) |
@@ -493,7 +493,7 b' class UserModel(BaseModel):' | |||
|
493 | 493 | log.error(traceback.format_exc()) |
|
494 | 494 | raise |
|
495 | 495 | |
|
496 | def reset_password_link(self, data): | |
|
496 | def reset_password_link(self, data, pwd_reset_url): | |
|
497 | 497 | from rhodecode.lib.celerylib import tasks, run_task |
|
498 | 498 | from rhodecode.model.notification import EmailNotificationModel |
|
499 | 499 | user_email = data['email'] |
@@ -502,12 +502,8 b' class UserModel(BaseModel):' | |||
|
502 | 502 | if user: |
|
503 | 503 | log.debug('password reset user found %s', user) |
|
504 | 504 | |
|
505 | password_reset_url = url( | |
|
506 | 'reset_password_confirmation', key=user.api_key, | |
|
507 | qualified=True) | |
|
508 | ||
|
509 | 505 | email_kwargs = { |
|
510 |
'password_reset_url': p |
|
|
506 | 'password_reset_url': pwd_reset_url, | |
|
511 | 507 | 'user': user, |
|
512 | 508 | 'email': user_email, |
|
513 | 509 | 'date': datetime.datetime.now() |
@@ -216,7 +216,13 b' class UserGroupModel(BaseModel):' | |||
|
216 | 216 | if 'user' in form_data: |
|
217 | 217 | owner = form_data['user'] |
|
218 | 218 | if isinstance(owner, basestring): |
|
219 |
|
|
|
219 | owner = User.get_by_username(form_data['user']) | |
|
220 | ||
|
221 | if not isinstance(owner, User): | |
|
222 | raise ValueError( | |
|
223 | 'invalid owner for user group: %s' % form_data['user']) | |
|
224 | ||
|
225 | user_group.user = owner | |
|
220 | 226 | |
|
221 | 227 | if 'users_group_members' in form_data: |
|
222 | 228 | members_id_list = self._clean_members_data( |
@@ -51,6 +51,11 b' class SearchParamsSchema(colander.Mappin' | |||
|
51 | 51 | colander.String(), |
|
52 | 52 | missing='content', |
|
53 | 53 | validator=colander.OneOf(['content', 'path', 'commit', 'repository'])) |
|
54 | search_sort = colander.SchemaNode( | |
|
55 | colander.String(), | |
|
56 | missing='newfirst', | |
|
57 | validator=colander.OneOf( | |
|
58 | ['oldfirst', 'newfirst'])) | |
|
54 | 59 | page_limit = colander.SchemaNode( |
|
55 | 60 | colander.Integer(), |
|
56 | 61 | missing=10, |
@@ -38,9 +38,11 b' from sqlalchemy.sql.expression import tr' | |||
|
38 | 38 | from sqlalchemy.util import OrderedSet |
|
39 | 39 | from webhelpers.pylonslib.secure_form import authentication_token |
|
40 | 40 | |
|
41 | from rhodecode.authentication import ( | |
|
42 | legacy_plugin_prefix, _import_legacy_plugin) | |
|
43 | from rhodecode.authentication.base import loadplugin | |
|
41 | 44 | from rhodecode.config.routing import ADMIN_PREFIX |
|
42 | 45 | from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny |
|
43 | from rhodecode.lib.exceptions import LdapImportError | |
|
44 | 46 | from rhodecode.lib.utils import repo_name_slug, make_db_config |
|
45 | 47 | from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5 |
|
46 | 48 | from rhodecode.lib.vcs.backends.git.repository import GitRepository |
@@ -437,8 +439,7 b' def ValidAuth():' | |||
|
437 | 439 | password = value['password'] |
|
438 | 440 | username = value['username'] |
|
439 | 441 | |
|
440 | if not authenticate(username, password, '', | |
|
441 | HTTP_TYPE, | |
|
442 | if not authenticate(username, password, '', HTTP_TYPE, | |
|
442 | 443 | skip_missing=True): |
|
443 | 444 | user = User.get_by_username(username) |
|
444 | 445 | if user and not user.active: |
@@ -448,7 +449,7 b' def ValidAuth():' | |||
|
448 | 449 | msg, value, state, error_dict={'username': msg} |
|
449 | 450 | ) |
|
450 | 451 | else: |
|
451 | log.warning('user %s failed to authenticate', username) | |
|
452 | log.warning('user `%s` failed to authenticate', username) | |
|
452 | 453 | msg = M(self, 'invalid_username', state) |
|
453 | 454 | msg2 = M(self, 'invalid_password', state) |
|
454 | 455 | raise formencode.Invalid( |
@@ -986,28 +987,71 b' def ValidAuthPlugins():' | |||
|
986 | 987 | 'import_duplicate': _( |
|
987 | 988 | u'Plugins %(loaded)s and %(next_to_load)s ' |
|
988 | 989 | u'both export the same name'), |
|
990 | 'missing_includeme': _( | |
|
991 | u'The plugin "%(plugin_id)s" is missing an includeme ' | |
|
992 | u'function.'), | |
|
993 | 'import_error': _( | |
|
994 | u'Can not load plugin "%(plugin_id)s"'), | |
|
995 | 'no_plugin': _( | |
|
996 | u'No plugin available with ID "%(plugin_id)s"'), | |
|
989 | 997 | } |
|
990 | 998 | |
|
991 | 999 | def _to_python(self, value, state): |
|
992 | 1000 | # filter empty values |
|
993 | 1001 | return filter(lambda s: s not in [None, ''], value) |
|
994 | 1002 | |
|
995 |
def validate_ |
|
|
996 | from rhodecode.authentication.base import loadplugin | |
|
997 | module_list = value | |
|
998 | unique_names = {} | |
|
1003 | def _validate_legacy_plugin_id(self, plugin_id, value, state): | |
|
1004 | """ | |
|
1005 | Validates that the plugin import works. It also checks that the | |
|
1006 | plugin has an includeme attribute. | |
|
1007 | """ | |
|
999 | 1008 | try: |
|
1000 | for module in module_list: | |
|
1001 | plugin = loadplugin(module) | |
|
1002 | plugin_name = plugin.name | |
|
1003 | if plugin_name in unique_names: | |
|
1004 | msg = M(self, 'import_duplicate', state, | |
|
1005 | loaded=unique_names[plugin_name], | |
|
1006 | next_to_load=plugin_name) | |
|
1007 | raise formencode.Invalid(msg, value, state) | |
|
1008 | unique_names[plugin_name] = plugin | |
|
1009 | except (KeyError, AttributeError, TypeError) as e: | |
|
1010 |
raise formencode.Invalid( |
|
|
1009 | plugin = _import_legacy_plugin(plugin_id) | |
|
1010 | except Exception as e: | |
|
1011 | log.exception( | |
|
1012 | 'Exception during import of auth legacy plugin "{}"' | |
|
1013 | .format(plugin_id)) | |
|
1014 | msg = M(self, 'import_error', plugin_id=plugin_id) | |
|
1015 | raise formencode.Invalid(msg, value, state) | |
|
1016 | ||
|
1017 | if not hasattr(plugin, 'includeme'): | |
|
1018 | msg = M(self, 'missing_includeme', plugin_id=plugin_id) | |
|
1019 | raise formencode.Invalid(msg, value, state) | |
|
1020 | ||
|
1021 | return plugin | |
|
1022 | ||
|
1023 | def _validate_plugin_id(self, plugin_id, value, state): | |
|
1024 | """ | |
|
1025 | Plugins are already imported during app start up. Therefore this | |
|
1026 | validation only retrieves the plugin from the plugin registry and | |
|
1027 | if it returns something not None everything is OK. | |
|
1028 | """ | |
|
1029 | plugin = loadplugin(plugin_id) | |
|
1030 | ||
|
1031 | if plugin is None: | |
|
1032 | msg = M(self, 'no_plugin', plugin_id=plugin_id) | |
|
1033 | raise formencode.Invalid(msg, value, state) | |
|
1034 | ||
|
1035 | return plugin | |
|
1036 | ||
|
1037 | def validate_python(self, value, state): | |
|
1038 | unique_names = {} | |
|
1039 | for plugin_id in value: | |
|
1040 | ||
|
1041 | # Validate legacy or normal plugin. | |
|
1042 | if plugin_id.startswith(legacy_plugin_prefix): | |
|
1043 | plugin = self._validate_legacy_plugin_id( | |
|
1044 | plugin_id, value, state) | |
|
1045 | else: | |
|
1046 | plugin = self._validate_plugin_id(plugin_id, value, state) | |
|
1047 | ||
|
1048 | # Only allow unique plugin names. | |
|
1049 | if plugin.name in unique_names: | |
|
1050 | msg = M(self, 'import_duplicate', state, | |
|
1051 | loaded=unique_names[plugin.name], | |
|
1052 | next_to_load=plugin) | |
|
1053 | raise formencode.Invalid(msg, value, state) | |
|
1054 | unique_names[plugin.name] = plugin | |
|
1011 | 1055 | |
|
1012 | 1056 | return _validator |
|
1013 | 1057 |
@@ -514,6 +514,26 b' div.search-code-body {' | |||
|
514 | 514 | .match { background-color: #faffa6;} |
|
515 | 515 | .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; } |
|
516 | 516 | } |
|
517 | .code-highlighttable { | |
|
518 | border-collapse: collapse; | |
|
519 | ||
|
520 | tr:hover { | |
|
521 | background: #fafafa; | |
|
522 | } | |
|
523 | td.code { | |
|
524 | padding-left: 10px; | |
|
525 | } | |
|
526 | td.line { | |
|
527 | border-right: 1px solid #ccc !important; | |
|
528 | padding-right: 10px; | |
|
529 | text-align: right; | |
|
530 | font-family: "Lucida Console",Monaco,monospace; | |
|
531 | span { | |
|
532 | white-space: pre-wrap; | |
|
533 | color: #666666; | |
|
534 | } | |
|
535 | } | |
|
536 | } | |
|
517 | 537 | } |
|
518 | 538 | |
|
519 | 539 | div.annotatediv { margin-left: 2px; margin-right: 4px; } |
@@ -353,7 +353,12 b'' | |||
|
353 | 353 | .middle-group{ |
|
354 | 354 | width: 10%; |
|
355 | 355 | text-align: center; |
|
356 |
padding-top: |
|
|
356 | padding-top: 4em; | |
|
357 | i { | |
|
358 | font-size: 18px; | |
|
359 | cursor: pointer; | |
|
360 | line-height: 2em; | |
|
361 | } | |
|
357 | 362 | } |
|
358 | 363 | |
|
359 | 364 | } |
@@ -1234,6 +1234,13 b' table.issuetracker {' | |||
|
1234 | 1234 | .reviewer { |
|
1235 | 1235 | float: left; |
|
1236 | 1236 | } |
|
1237 | ||
|
1238 | &.to-delete { | |
|
1239 | .user, | |
|
1240 | .reviewer { | |
|
1241 | text-decoration: line-through; | |
|
1242 | } | |
|
1243 | } | |
|
1237 | 1244 | } |
|
1238 | 1245 | |
|
1239 | 1246 | .reviewer_member_remove { |
@@ -80,6 +80,11 b'' | |||
|
80 | 80 | [tag="recommends"] { &:extend(.tag7); } |
|
81 | 81 | [tag="see"] { &:extend(.tag8); } |
|
82 | 82 | |
|
83 | .perm_overriden { | |
|
84 | text-decoration: line-through; | |
|
85 | opacity: 0.6; | |
|
86 | } | |
|
87 | ||
|
83 | 88 | .perm_tag { |
|
84 | 89 | &:extend(.tag); |
|
85 | 90 |
@@ -1,45 +1,50 b'' | |||
|
1 | /* This file is automatically generated. DO NOT change it manually. | |
|
2 | * If this file needs to be modified, edit | |
|
3 | * rhodecode/utils/file_generation/js_routes_data.py | |
|
4 | * and run the script invoke -r scripts/ generate.js-routes . | |
|
5 | */ | |
|
1 | ||
|
2 | /****************************************************************************** | |
|
3 | * * | |
|
4 | * DO NOT CHANGE THIS FILE MANUALLY * | |
|
5 | * * | |
|
6 | * * | |
|
7 | * This file is automatically generated when the app starts up. * | |
|
8 | * * | |
|
9 | * To add a route here pass jsroute=True to the route definition in the app * | |
|
10 | * * | |
|
11 | ******************************************************************************/ | |
|
6 | 12 | function registerRCRoutes() { |
|
7 | 13 | // routes registration |
|
8 | 14 | pyroutes.register('home', '/', []); |
|
9 |
pyroutes.register(' |
|
|
10 | pyroutes.register('gists', '/_admin/gists', []); | |
|
15 | pyroutes.register('user_autocomplete_data', '/_users', []); | |
|
11 | 16 | pyroutes.register('new_repo', '/_admin/create_repository', []); |
|
12 | pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']); | |
|
13 | pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']); | |
|
14 | pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
|
17 | pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); | |
|
18 | pyroutes.register('gists', '/_admin/gists', []); | |
|
19 | pyroutes.register('new_gist', '/_admin/gists/new', []); | |
|
20 | pyroutes.register('toggle_following', '/_admin/toggle_following', []); | |
|
21 | pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); | |
|
22 | pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']); | |
|
23 | pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); | |
|
24 | pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']); | |
|
15 | 25 | pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); |
|
16 | 26 | pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']); |
|
17 | pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); | |
|
18 | pyroutes.register('user_autocomplete_data', '/_users', []); | |
|
19 | pyroutes.register('toggle_following', '/_admin/toggle_following', []); | |
|
20 | pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); | |
|
21 | pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']); | |
|
22 | pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']); | |
|
23 | 27 | pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']); |
|
24 | 28 | pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']); |
|
25 | 29 | pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); |
|
26 |
pyroutes.register(' |
|
|
27 | pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); | |
|
30 | pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']); | |
|
31 | pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']); | |
|
32 | pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); | |
|
33 | pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']); | |
|
34 | pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']); | |
|
35 | pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']); | |
|
36 | pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); | |
|
37 | pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); | |
|
38 | pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']); | |
|
39 | pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); | |
|
40 | pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']); | |
|
41 | pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
|
42 | pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
|
43 | pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
|
44 | pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
|
28 | 45 | pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']); |
|
29 | 46 | pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); |
|
30 | 47 | pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); |
|
31 |
pyroutes.register(' |
|
|
32 |
pyroutes.register(' |
|
|
33 | pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); | |
|
34 | pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']); | |
|
35 | pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); | |
|
36 | pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); | |
|
37 | pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']); | |
|
38 | pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); | |
|
39 | pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); | |
|
40 | pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']); | |
|
41 | pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']); | |
|
42 | pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']); | |
|
48 | pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']); | |
|
49 | pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']); | |
|
43 | 50 | } |
|
44 | ||
|
45 | registerRCRoutes(); No newline at end of file |
@@ -190,7 +190,7 b' var AgeModule = (function () {' | |||
|
190 | 190 | |
|
191 | 191 | }, |
|
192 | 192 | createTimeComponent: function(dateTime, text) { |
|
193 | return '<time class="timeago tooltip" title="{1}" datetime="{0}">{1}</time>'.format(dateTime, text); | |
|
193 | return '<time class="timeago tooltip" title="{1}" datetime="{0}+0000">{1}</time>'.format(dateTime, text); | |
|
194 | 194 | } |
|
195 | 195 | } |
|
196 | 196 | })(); |
@@ -30,7 +30,6 b' var removeReviewMember = function(review' | |||
|
30 | 30 | if (reviewer){ |
|
31 | 31 | // mark as to-remove |
|
32 | 32 | var obj = $('#reviewer_{0}_name'.format(reviewer_id)); |
|
33 | obj.css("text-decoration", "line-through"); | |
|
34 | 33 | obj.addClass('to-delete'); |
|
35 | 34 | // now delete the input |
|
36 | 35 | $('#reviewer_{0}_input'.format(reviewer_id)).remove(); |
@@ -20,9 +20,11 b'' | |||
|
20 | 20 | |
|
21 | 21 | |
|
22 | 22 | import pylons |
|
23 | from pyramid.i18n import get_localizer, TranslationStringFactory | |
|
24 | 23 | |
|
25 | tsf = TranslationStringFactory('rc_root') | |
|
24 | from pyramid.i18n import get_localizer | |
|
25 | from pyramid.threadlocal import get_current_request | |
|
26 | ||
|
27 | from rhodecode.translation import _ as tsf | |
|
26 | 28 | |
|
27 | 29 | |
|
28 | 30 | def add_renderer_globals(event): |
@@ -33,8 +35,11 b' def add_renderer_globals(event):' | |||
|
33 | 35 | event['c'] = pylons.tmpl_context |
|
34 | 36 | event['url'] = pylons.url |
|
35 | 37 | |
|
38 | # TODO: When executed in pyramid view context the request is not available | |
|
39 | # in the event. Find a better solution to get the request. | |
|
40 | request = event['request'] or get_current_request() | |
|
41 | ||
|
36 | 42 | # Add Pyramid translation as '_' to context |
|
37 | request = event['request'] | |
|
38 | 43 | event['_'] = request.translate |
|
39 | 44 | event['localizer'] = request.localizer |
|
40 | 45 |
@@ -49,46 +49,44 b'' | |||
|
49 | 49 | <div class="fields"> |
|
50 | 50 | ${h.secure_form(request.resource_path(resource, route_name='auth_home'))} |
|
51 | 51 | <div class="form"> |
|
52 | ||
|
52 | 53 | %for node in plugin.get_settings_schema(): |
|
53 | <% label_cls = ("label-checkbox" if (node.widget == "bool") else "") %> | |
|
54 | <% label_css_class = ("label-checkbox" if (node.widget == "bool") else "") %> | |
|
54 | 55 | <div class="field"> |
|
55 | <div class="label ${label_cls}"><label for="${node.name}">${node.title}</label></div> | |
|
56 | %if node.widget in ["string", "int", "unicode"]: | |
|
57 | <div class="input"> | |
|
58 | ${h.text(node.name, class_="medium")} | |
|
59 | <p class="help-block">${node.description}</p> | |
|
60 | </div> | |
|
61 |
%elif node.widget == " |
|
|
62 | <div class="input"> | |
|
63 | ${h.password(node.name, class_="medium")} | |
|
64 | <p class="help-block">${node.description}</p> | |
|
65 |
|
|
|
66 | %elif node.widget == "bool": | |
|
67 | <div class="input"> | |
|
68 | <div class="checkbox">${h.checkbox(node.name, True)}</div> | |
|
69 | <span class="help-block">${node.description}</span> | |
|
70 | </div> | |
|
71 | %elif node.widget == "select": | |
|
72 | <div class="select"> | |
|
73 | ${h.select(node.name, node.default, node.validator.choices)} | |
|
74 | <p class="help-block">${node.description}</p> | |
|
75 | </div> | |
|
76 | %elif node.widget == "readonly": | |
|
77 | <div class="input"> | |
|
56 | <div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div> | |
|
57 | <div class="input"> | |
|
58 | %if node.widget in ["string", "int", "unicode"]: | |
|
59 | ${h.text(node.name, defaults.get(node.name), class_="medium")} | |
|
60 | %elif node.widget == "password": | |
|
61 | ${h.password(node.name, defaults.get(node.name), class_="medium")} | |
|
62 | %elif node.widget == "bool": | |
|
63 | <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div> | |
|
64 | %elif node.widget == "select": | |
|
65 | ${h.select(node.name, defaults.get(node.name), node.validator.choices)} | |
|
66 | %elif node.widget == "readonly": | |
|
78 | 67 | ${node.default} |
|
79 | <p class="help-block">${node.description}</p> | |
|
80 | </div> | |
|
81 | %else: | |
|
82 | <div class="input"> | |
|
68 | %else: | |
|
83 | 69 | This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select]. |
|
84 | <p class="help-block">${node.description}</p> | |
|
85 |
|
|
|
86 | %endif | |
|
70 | %endif | |
|
71 | %if node.name in errors: | |
|
72 | <span class="error-message">${errors.get(node.name)}</span> | |
|
73 | <br /> | |
|
74 | %endif | |
|
75 | <p class="help-block">${node.description}</p> | |
|
76 | </div> | |
|
87 | 77 | </div> |
|
88 | 78 | %endfor |
|
79 | ||
|
80 | ## Allow derived templates to add something below the form | |
|
81 | ## input fields | |
|
82 | %if hasattr(next, 'below_form_fields'): | |
|
83 | ${next.below_form_fields()} | |
|
84 | %endif | |
|
85 | ||
|
89 | 86 | <div class="buttons"> |
|
90 | 87 | ${h.submit('save',_('Save'),class_="btn")} |
|
91 | 88 | </div> |
|
89 | ||
|
92 | 90 | </div> |
|
93 | 91 | ${h.end_form()} |
|
94 | 92 | </div> |
@@ -66,7 +66,7 b'' | |||
|
66 | 66 | %if c.gist.gist_expires == -1: |
|
67 | 67 | ${_('never')} |
|
68 | 68 | %else: |
|
69 | ${h.age_component(h.time_to_datetime(c.gist.gist_expires))} | |
|
69 | ${h.age_component(h.time_to_utcdatetime(c.gist.gist_expires))} | |
|
70 | 70 | %endif |
|
71 | 71 | </span> |
|
72 | 72 | </div> |
@@ -29,7 +29,11 b'' | |||
|
29 | 29 | <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('My Profile')}</a></li> |
|
30 | 30 | <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li> |
|
31 | 31 | <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li> |
|
32 | <li class="${'active' if c.active=='oauth' else ''}"><a href="${h.url('my_account_oauth')}">${_('OAuth Identities')}</a></li> | |
|
32 | ## TODO: Find a better integration of oauth views into navigation. | |
|
33 | %try: | |
|
34 | <li class="${'active' if c.active=='oauth' else ''}"><a href="${h.route_path('my_account_oauth')}">${_('OAuth Identities')}</a></li> | |
|
35 | %except KeyError: | |
|
36 | %endtry | |
|
33 | 37 | <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li> |
|
34 | 38 | <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li> |
|
35 | 39 | <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li> |
@@ -42,9 +42,9 b'' | |||
|
42 | 42 | ${_('expires')}: ${_('never')} |
|
43 | 43 | %else: |
|
44 | 44 | %if auth_token.expired: |
|
45 | ${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} | |
|
45 | ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
|
46 | 46 | %else: |
|
47 | ${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} | |
|
47 | ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
|
48 | 48 | %endif |
|
49 | 49 | %endif |
|
50 | 50 | </td> |
@@ -50,19 +50,19 b'' | |||
|
50 | 50 | if (resp.status == 200) { |
|
51 | 51 | var jsonResponse = resp.responseJSON; |
|
52 | 52 | |
|
53 | if (jsonResponse === undefined){ | |
|
54 | setTimeout(function(){ | |
|
53 | if (jsonResponse === undefined) { | |
|
54 | setTimeout(function () { | |
|
55 | 55 | // we might have a backend problem, try dashboard again |
|
56 | 56 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; |
|
57 |
}, |
|
|
58 | } | |
|
59 | ||
|
60 | if (skipCheck || jsonResponse.result === true) { | |
|
61 | // success, means go to dashboard | |
|
62 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; | |
|
57 | }, 3000); | |
|
63 | 58 | } else { |
|
64 | // Schedule the next request when the current one's complete | |
|
65 | setTimeout(worker, 1000); | |
|
59 | if (skipCheck || jsonResponse.result === true) { | |
|
60 | // success, means go to dashboard | |
|
61 | window.location = "${h.url('summary_home', repo_name = c.repo)}"; | |
|
62 | } else { | |
|
63 | // Schedule the next request when the current one's complete | |
|
64 | setTimeout(worker, 1000); | |
|
65 | } | |
|
66 | 66 | } |
|
67 | 67 | } |
|
68 | 68 | else { |
@@ -43,11 +43,14 b'' | |||
|
43 | 43 | </div> |
|
44 | 44 | <div class="field"> |
|
45 | 45 | <div class="label"> |
|
46 |
<label for="users_group_active">${_(' |
|
|
46 | <label for="users_group_active">${_('Search')}:</label> | |
|
47 | ${h.text('from_user_group', | |
|
48 | placeholder="user/usergroup", | |
|
49 | class_="medium")} | |
|
47 | 50 | </div> |
|
48 | 51 | <div class="select side-by-side-selector"> |
|
49 | 52 | <div class="left-group"> |
|
50 |
<label class="text" |
|
|
53 | <label class="text"><strong>${_('Chosen group members')}</strong></label> | |
|
51 | 54 | ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)} |
|
52 | 55 | <div class="btn" id="remove_all_elements" > |
|
53 | 56 | ${_('Remove all elements')} |
@@ -60,7 +63,8 b'' | |||
|
60 | 63 | <i id="remove_element" class="icon-chevron-right"></i> |
|
61 | 64 | </div> |
|
62 | 65 | <div class="right-group"> |
|
63 |
<label class="text" >${_('Available |
|
|
66 | <label class="text" >${_('Available users')} | |
|
67 | </label> | |
|
64 | 68 | ${h.select('available_members',[],c.available_members,multiple=True,size=8,)} |
|
65 | 69 | <div class="btn" id="add_all_elements" > |
|
66 | 70 | <i class="icon-chevron-left"></i>${_('Add all elements')} |
@@ -86,6 +90,42 b'' | |||
|
86 | 90 | 'dropdownAutoWidth': true |
|
87 | 91 | }); |
|
88 | 92 | |
|
93 | $('#from_user_group').autocomplete({ | |
|
94 | serviceUrl: pyroutes.url('user_autocomplete_data'), | |
|
95 | minChars:2, | |
|
96 | maxHeight:400, | |
|
97 | width:300, | |
|
98 | deferRequestBy: 300, //miliseconds | |
|
99 | showNoSuggestionNotice: true, | |
|
100 | params: { user_groups:true }, | |
|
101 | formatResult: autocompleteFormatResult, | |
|
102 | lookupFilter: autocompleteFilterResult, | |
|
103 | onSelect: function(element, suggestion){ | |
|
104 | ||
|
105 | function preSelectUserIds(uids) { | |
|
106 | $('#available_members').val(uids); | |
|
107 | $('#users_group_members').val(uids); | |
|
108 | } | |
|
109 | ||
|
110 | if (suggestion.value_type == 'user_group') { | |
|
111 | $.getJSON( | |
|
112 | pyroutes.url('edit_user_group_members', | |
|
113 | {'user_group_id': suggestion.id}), | |
|
114 | function(data) { | |
|
115 | var uids = []; | |
|
116 | $.each(data.members, function(idx, user) { | |
|
117 | var userid = user[0], | |
|
118 | username = user[1]; | |
|
119 | uids.push(userid.toString()); | |
|
120 | }); | |
|
121 | preSelectUserIds(uids) | |
|
122 | } | |
|
123 | ); | |
|
124 | } else if (suggestion.value_type == 'user') { | |
|
125 | preSelectUserIds([suggestion.id.toString()]); | |
|
126 | } | |
|
127 | } | |
|
128 | }); | |
|
89 | 129 | UsersAutoComplete('user', '${c.rhodecode_user.user_id}'); |
|
90 | 130 | }) |
|
91 | 131 | </script> |
@@ -38,9 +38,9 b'' | |||
|
38 | 38 | ${_('expires')}: ${_('never')} |
|
39 | 39 | %else: |
|
40 | 40 | %if auth_token.expired: |
|
41 | ${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} | |
|
41 | ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
|
42 | 42 | %else: |
|
43 | ${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))} | |
|
43 | ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))} | |
|
44 | 44 | %endif |
|
45 | 45 | %endif |
|
46 | 46 | </td> |
@@ -297,7 +297,7 b'' | |||
|
297 | 297 | <div id="quick_login"> |
|
298 | 298 | %if c.rhodecode_user.username == h.DEFAULT_USER: |
|
299 | 299 | <h4>${_('Sign in to your account')}</h4> |
|
300 |
${h.form(h. |
|
|
300 | ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)} | |
|
301 | 301 | <div class="form form-vertical"> |
|
302 | 302 | <div class="fields"> |
|
303 | 303 | <div class="field"> |
@@ -312,7 +312,7 b'' | |||
|
312 | 312 | <div class="field"> |
|
313 | 313 | <div class="label"> |
|
314 | 314 | <label for="password">${_('Password')}:</label> |
|
315 |
<span class="forgot_password">${h.link_to(_('(Forgot password?)'),h. |
|
|
315 | <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'))}</span> | |
|
316 | 316 | </div> |
|
317 | 317 | <div class="input"> |
|
318 | 318 | ${h.password('password',class_='focus',tabindex=2)} |
@@ -321,7 +321,7 b'' | |||
|
321 | 321 | <div class="buttons"> |
|
322 | 322 | <div class="register"> |
|
323 | 323 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): |
|
324 |
${h.link_to(_("Don't have an account ?"),h. |
|
|
324 | ${h.link_to(_("Don't have an account ?"),h.route_path('register'))} | |
|
325 | 325 | %endif |
|
326 | 326 | </div> |
|
327 | 327 | <div class="submit"> |
@@ -341,7 +341,7 b'' | |||
|
341 | 341 | <ol class="links"> |
|
342 | 342 | <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li> |
|
343 | 343 | <li class="logout"> |
|
344 |
${h.secure_form(h. |
|
|
344 | ${h.secure_form(h.route_path('logout'))} | |
|
345 | 345 | ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")} |
|
346 | 346 | ${h.end_form()} |
|
347 | 347 | </li> |
@@ -455,8 +455,11 b'' | |||
|
455 | 455 | tmpl += '<i class="icon-unlock-alt"></i> '; |
|
456 | 456 | } |
|
457 | 457 | } |
|
458 | if(obj_dict && state.type == 'commit') { | |
|
459 | tmpl += '<i class="icon-tag"></i>'; | |
|
460 | } | |
|
458 | 461 | if(obj_dict && state.type == 'group'){ |
|
459 |
|
|
|
462 | tmpl += '<i class="icon-folder-close"></i> '; | |
|
460 | 463 | } |
|
461 | 464 | tmpl += escapeMarkup(state.text); |
|
462 | 465 | return tmpl; |
@@ -496,7 +499,7 b'' | |||
|
496 | 499 | query.callback({results: cachedData.results}); |
|
497 | 500 | } else { |
|
498 | 501 | $.ajax({ |
|
499 |
url: "${h.url(' |
|
|
502 | url: "${h.url('goto_switcher_data')}", | |
|
500 | 503 | data: {'query': query.term}, |
|
501 | 504 | dataType: 'json', |
|
502 | 505 | type: 'GET', |
@@ -514,7 +517,7 b'' | |||
|
514 | 517 | |
|
515 | 518 | $("#repo_switcher").on('select2-selecting', function(e){ |
|
516 | 519 | e.preventDefault(); |
|
517 | window.location = pyroutes.url('summary_home', {'repo_name': e.val}); | |
|
520 | window.location = e.choice.url; | |
|
518 | 521 | }); |
|
519 | 522 | |
|
520 | 523 | ## Global mouse bindings ## |
@@ -4,7 +4,6 b'' | |||
|
4 | 4 | ## ${p.perms_summary(c.perm_user.permissions)} |
|
5 | 5 | |
|
6 | 6 | <%def name="perms_summary(permissions, show_all=False, actions=True)"> |
|
7 | ||
|
8 | 7 | <div id="perms" class="table fields"> |
|
9 | 8 | %for section in sorted(permissions.keys()): |
|
10 | 9 | <div class="panel panel-default"> |
@@ -134,7 +133,15 b'' | |||
|
134 | 133 | %endif |
|
135 | 134 | </td> |
|
136 | 135 | <td class="td-tags"> |
|
136 | %if hasattr(permissions[section], 'perm_origin_stack'): | |
|
137 | %for i, (perm, origin) in enumerate(reversed(permissions[section].perm_origin_stack[k])): | |
|
138 | <span class="${i > 0 and 'perm_overriden' or ''} perm_tag ${perm.split('.')[-1]}"> | |
|
139 | ${perm} (${origin}) | |
|
140 | </span> | |
|
141 | %endfor | |
|
142 | %else: | |
|
137 | 143 | <span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span> |
|
144 | %endif | |
|
138 | 145 | </td> |
|
139 | 146 | %if actions: |
|
140 | 147 | <td class="td-action"> |
@@ -154,7 +161,7 b'' | |||
|
154 | 161 | <tr id="empty_${section}" class="noborder" style="display:none;"> |
|
155 | 162 | <td colspan="6">${_('No permission defined')}</td> |
|
156 | 163 | </tr> |
|
157 | ||
|
164 | ||
|
158 | 165 | </tbody> |
|
159 | 166 | %endif |
|
160 | 167 | </table> |
@@ -115,6 +115,7 b'' | |||
|
115 | 115 | <!--[if lt IE 9]> |
|
116 | 116 | <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script> |
|
117 | 117 | <![endif]--> |
|
118 | <script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script> | |
|
118 | 119 | <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script> |
|
119 | 120 | <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script> |
|
120 | 121 |
@@ -12,7 +12,7 b'' | |||
|
12 | 12 | ${base.gravatar_with_user(comment.author.email, 16)} |
|
13 | 13 | </div> |
|
14 | 14 | <div class="date"> |
|
15 | ${h.age_component(comment.modified_at)} | |
|
15 | ${h.age_component(comment.modified_at, time_is_local=True)} | |
|
16 | 16 | </div> |
|
17 | 17 | <div class="status-change"> |
|
18 | 18 | %if comment.pull_request: |
@@ -80,7 +80,7 b'' | |||
|
80 | 80 | ${base.gravatar_with_user(comment.author.email, 16)} |
|
81 | 81 | </div> |
|
82 | 82 | <div class="date"> |
|
83 | ${h.age_component(comment.modified_at)} | |
|
83 | ${h.age_component(comment.modified_at, time_is_local=True)} | |
|
84 | 84 | </div> |
|
85 | 85 | %if comment.status_change: |
|
86 | 86 | <span class="changeset-status-container"> |
@@ -243,7 +243,7 b'' | |||
|
243 | 243 | |
|
244 | 244 | <%def name="gist_created(created_on)"> |
|
245 | 245 | <div class="created"> |
|
246 | ${h.age_component(created_on)} | |
|
246 | ${h.age_component(created_on, time_is_local=True)} | |
|
247 | 247 | </div> |
|
248 | 248 | </%def> |
|
249 | 249 | |
@@ -252,7 +252,7 b'' | |||
|
252 | 252 | %if expires == -1: |
|
253 | 253 | ${_('never')} |
|
254 | 254 | %else: |
|
255 | ${h.age_component(h.time_to_datetime(expires))} | |
|
255 | ${h.age_component(h.time_to_utcdatetime(expires))} | |
|
256 | 256 | %endif |
|
257 | 257 | </div> |
|
258 | 258 | </%def> |
@@ -289,7 +289,7 b'' | |||
|
289 | 289 | </%def> |
|
290 | 290 | |
|
291 | 291 | <%def name="pullrequest_updated_on(updated_on)"> |
|
292 | ${h.age_component(h.time_to_datetime(updated_on))} | |
|
292 | ${h.age_component(h.time_to_utcdatetime(updated_on))} | |
|
293 | 293 | </%def> |
|
294 | 294 | |
|
295 | 295 | <%def name="pullrequest_author(full_contact)"> |
@@ -11,7 +11,7 b'' | |||
|
11 | 11 | ${base.gravatar_with_user(f.user.email, 16)} |
|
12 | 12 | </td> |
|
13 | 13 | <td class="td-time follower_date"> |
|
14 | ${h.age_component(f.follows_from)} | |
|
14 | ${h.age_component(f.follows_from, time_is_local=True)} | |
|
15 | 15 | </td> |
|
16 | 16 | </tr> |
|
17 | 17 | % endfor |
@@ -22,7 +22,7 b'' | |||
|
22 | 22 | <div class="truncate">${f.description}</div> |
|
23 | 23 | </td> |
|
24 | 24 | <td class="td-time follower_date"> |
|
25 | ${h.age_component(f.created_on)} | |
|
25 | ${h.age_component(f.created_on, time_is_local=True)} | |
|
26 | 26 | </td> |
|
27 | 27 | <td class="td-compare"> |
|
28 | 28 | <a title="${_('Compare fork with %s' % c.repo_name)}" |
@@ -28,7 +28,7 b'' | |||
|
28 | 28 | </div> |
|
29 | 29 | <div class="journal_action_params">${h.literal(h.action_parser(entry)[1]())}</div> |
|
30 | 30 | <div class="date"> |
|
31 | ${h.age_component(entry.action_date)} | |
|
31 | ${h.age_component(entry.action_date, time_is_local=True)} | |
|
32 | 32 | </div> |
|
33 | 33 | %endfor |
|
34 | 34 | </div> |
@@ -1,6 +1,5 b'' | |||
|
1 | 1 | ## -*- coding: utf-8 -*- |
|
2 | 2 | <%inherit file="base/root.html"/> |
|
3 | <%namespace file="base/social_buttons.html" import="render_social_buttons"/> | |
|
4 | 3 | |
|
5 | 4 | <%def name="title()"> |
|
6 | 5 | ${_('Sign In')} |
@@ -35,21 +34,35 b'' | |||
|
35 | 34 | <div class="sign-in-title"> |
|
36 | 35 | <h1>${_('Sign In')}</h1> |
|
37 | 36 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): |
|
38 |
<h4>${h.link_to(_("Go to the registration page to create a new account."), |
|
|
37 | <h4>${h.link_to(_("Go to the registration page to create a new account."), request.route_path('register'))}</h4> | |
|
39 | 38 | %endif |
|
40 | 39 | </div> |
|
41 | 40 | <div class="inner form"> |
|
42 |
${h.form( |
|
|
41 | ${h.form(request.route_path('login', _query={'came_from': came_from}), needs_csrf_token=False)} | |
|
42 | ||
|
43 | 43 | <label for="username">${_('Username')}:</label> |
|
44 | ${h.text('username',class_='focus')} | |
|
44 | ${h.text('username', class_='focus', value=defaults.get('username'))} | |
|
45 | %if 'username' in errors: | |
|
46 | <span class="error-message">${errors.get('username')}</span> | |
|
47 | <br /> | |
|
48 | %endif | |
|
49 | ||
|
45 | 50 | <label for="password">${_('Password')}:</label> |
|
46 | ${h.password('password',class_='focus')} | |
|
47 | <input type="checkbox" id="remember" name="remember" /> | |
|
51 | ${h.password('password', class_='focus')} | |
|
52 | %if 'password' in errors: | |
|
53 | <span class="error-message">${errors.get('password')}</span> | |
|
54 | <br /> | |
|
55 | %endif | |
|
56 | ||
|
57 | ${h.checkbox('remember', value=True, checked=defaults.get('remember'))} | |
|
48 | 58 | <label class="checkbox" for="remember">${_('Remember me')}</label> |
|
59 | ||
|
49 | 60 | <p class="links"> |
|
50 |
${h.link_to(_('Forgot your password?'), |
|
|
61 | ${h.link_to(_('Forgot your password?'), h.route_path('reset_password'))} | |
|
51 | 62 | </p> |
|
52 | ${h.submit('sign_in',_('Sign In'),class_="btn sign-in")} | |
|
63 | ||
|
64 | ${h.submit('sign_in', _('Sign In'), class_="btn sign-in")} | |
|
65 | ||
|
53 | 66 | ${h.end_form()} |
|
54 | 67 | <script type="text/javascript"> |
|
55 | 68 | $(document).ready(function(){ |
@@ -57,16 +70,8 b'' | |||
|
57 | 70 | }) |
|
58 | 71 | </script> |
|
59 | 72 | </div> |
|
60 | ||
|
61 | % if c.social_plugins: | |
|
62 | <p>${_('Sign In using one of external services')}:</p> | |
|
63 | ||
|
64 | <p> | |
|
65 | ${render_social_buttons(c.social_plugins, 'login')} | |
|
66 | </p> | |
|
67 | % endif | |
|
68 | ||
|
69 | 73 | <!-- end login --> |
|
74 | <%block name="below_login_button" /> | |
|
70 | 75 | </div> |
|
71 | 76 | </div> |
|
72 | 77 | </div> |
@@ -33,22 +33,30 b'' | |||
|
33 | 33 | <!-- login --> |
|
34 | 34 | <div class="sign-in-title"> |
|
35 | 35 | <h1>${_('Reset your Password')}</h1> |
|
36 |
<h4>${h.link_to(_("Go to the login page to sign in."), |
|
|
36 | <h4>${h.link_to(_("Go to the login page to sign in."), request.route_path('login'))}</h4> | |
|
37 | 37 | </div> |
|
38 | 38 | <div class="inner form"> |
|
39 |
${h.form( |
|
|
39 | ${h.form(request.route_path('reset_password'), needs_csrf_token=False)} | |
|
40 | 40 | <label for="email">${_('Email Address')}:</label> |
|
41 | ${h.text('email')} | |
|
41 | ${h.text('email', defaults.get('email'))} | |
|
42 | %if 'email' in errors: | |
|
43 | <span class="error-message">${errors.get('email')}</span> | |
|
44 | <br /> | |
|
45 | %endif | |
|
42 | 46 | |
|
43 |
%if c |
|
|
47 | %if captcha_active: | |
|
44 | 48 | <div class="login-captcha" |
|
45 | 49 | <label for="email">${_('Captcha')}:</label> |
|
46 | 50 | ${h.hidden('recaptcha_field')} |
|
47 | 51 | <div id="recaptcha"></div> |
|
52 | %if 'recaptcha_field' in errors: | |
|
53 | <span class="error-message">${errors.get('recaptcha_field')}</span> | |
|
54 | <br /> | |
|
55 | %endif | |
|
48 | 56 | </div> |
|
49 | 57 | %endif |
|
50 | 58 | |
|
51 | ${h.submit('send',_('Send password reset email'),class_="btn sign-in")} | |
|
59 | ${h.submit('send', _('Send password reset email'), class_="btn sign-in")} | |
|
52 | 60 | <div class="activation_msg">${_('Password reset link will be send to matching email address')}</div> |
|
53 | 61 | |
|
54 | 62 | ${h.end_form()} |
@@ -57,14 +65,14 b'' | |||
|
57 | 65 | </div> |
|
58 | 66 | </div> |
|
59 | 67 | |
|
60 |
%if c |
|
|
68 | %if captcha_active: | |
|
61 | 69 | <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> |
|
62 | 70 | %endif |
|
63 | 71 | <script type="text/javascript"> |
|
64 | 72 | $(document).ready(function(){ |
|
65 | 73 | $('#email').focus(); |
|
66 |
%if |
|
|
67 |
Recaptcha.create("${c |
|
|
74 | %if captcha_active: | |
|
75 | Recaptcha.create("${captcha_public_key}", "recaptcha", {theme: "white"}); | |
|
68 | 76 | %endif |
|
69 | 77 | }); |
|
70 | </script> No newline at end of file | |
|
78 | </script> |
@@ -271,7 +271,8 b'' | |||
|
271 | 271 | 'source_ref_type': 'rev', |
|
272 | 272 | 'target_ref': sourceRef[2], |
|
273 | 273 | 'target_ref_type': 'rev', |
|
274 | 'merge': true | |
|
274 | 'merge': true, | |
|
275 | '_': Date.now() // bypass browser caching | |
|
275 | 276 | }; // gather the source/target ref and repo here |
|
276 | 277 | |
|
277 | 278 | if (sourceRef.length !== 3 || targetRef.length !== 3) { |
@@ -1,6 +1,5 b'' | |||
|
1 | 1 | ## -*- coding: utf-8 -*- |
|
2 | 2 | <%inherit file="base/root.html"/> |
|
3 | <%namespace file="base/social_buttons.html" import="render_social_buttons"/> | |
|
4 | 3 | |
|
5 | 4 | <%def name="title()"> |
|
6 | 5 | ${_('Create an Account')} |
@@ -34,65 +33,91 b'' | |||
|
34 | 33 | <!-- login --> |
|
35 | 34 | <div class="sign-in-title"> |
|
36 | 35 | <h1>${_('Create an account')}</h1> |
|
37 |
<h4>${h.link_to(_("Go to the login page to sign in with an existing account."), |
|
|
36 | <h4>${h.link_to(_("Go to the login page to sign in with an existing account."), request.route_path('login'))}</h4> | |
|
38 | 37 | </div> |
|
39 | 38 | <div class="inner form"> |
|
40 |
${h.form( |
|
|
39 | ${h.form(request.route_path('register'), needs_csrf_token=False)} | |
|
40 | ||
|
41 | 41 | <label for="username">${_('Username')}:</label> |
|
42 |
${h.text('username', |
|
|
42 | ${h.text('username', defaults.get('username'))} | |
|
43 | %if 'username' in errors: | |
|
44 | <span class="error-message">${errors.get('username')}</span> | |
|
45 | <br /> | |
|
46 | %endif | |
|
47 | ||
|
43 | 48 | <label for="password">${_('Password')}:</label> |
|
44 |
${h.password('password', |
|
|
45 | <label for="password">${_('Re-enter password')}:</label> | |
|
46 | ${h.password('password_confirmation', c.form_data.get('password'))} | |
|
49 | ${h.password('password', defaults.get('password'))} | |
|
50 | %if 'password' in errors: | |
|
51 | <span class="error-message">${errors.get('password')}</span> | |
|
52 | <br /> | |
|
53 | %endif | |
|
54 | ||
|
55 | <label for="password_confirmation">${_('Re-enter password')}:</label> | |
|
56 | ${h.password('password_confirmation', defaults.get('password_confirmation'))} | |
|
57 | %if 'password_confirmation' in errors: | |
|
58 | <span class="error-message">${errors.get('password_confirmation')}</span> | |
|
59 | <br /> | |
|
60 | %endif | |
|
61 | ||
|
47 | 62 | <label for="firstname">${_('First Name')}:</label> |
|
48 | ${h.text('firstname')} | |
|
63 | ${h.text('firstname', defaults.get('firstname'))} | |
|
64 | %if 'firstname' in errors: | |
|
65 | <span class="error-message">${errors.get('firstname')}</span> | |
|
66 | <br /> | |
|
67 | %endif | |
|
68 | ||
|
49 | 69 | <label for="lastname">${_('Last Name')}:</label> |
|
50 | ${h.text('lastname')} | |
|
70 | ${h.text('lastname', defaults.get('lastname'))} | |
|
71 | %if 'lastname' in errors: | |
|
72 | <span class="error-message">${errors.get('lastname')}</span> | |
|
73 | <br /> | |
|
74 | %endif | |
|
75 | ||
|
51 | 76 | <label for="email">${_('Email')}:</label> |
|
52 |
${h.text('email', |
|
|
77 | ${h.text('email', defaults.get('email'))} | |
|
78 | %if 'email' in errors: | |
|
79 | <span class="error-message">${errors.get('email')}</span> | |
|
80 | <br /> | |
|
81 | %endif | |
|
53 | 82 | |
|
54 |
%if c |
|
|
83 | %if captcha_active: | |
|
55 | 84 | <div> |
|
56 |
<label for=" |
|
|
85 | <label for="recaptcha">${_('Captcha')}:</label> | |
|
57 | 86 | ${h.hidden('recaptcha_field')} |
|
58 | 87 | <div id="recaptcha"></div> |
|
88 | %if 'recaptcha_field' in errors: | |
|
89 | <span class="error-message">${errors.get('recaptcha_field')}</span> | |
|
90 | <br /> | |
|
91 | %endif | |
|
59 | 92 | </div> |
|
60 | 93 | %endif |
|
61 | 94 | |
|
62 |
%if not |
|
|
95 | %if not auto_active: | |
|
63 | 96 | <p class="activation_msg"> |
|
64 | 97 | ${_('Account activation requires admin approval.')} |
|
65 | 98 | </p> |
|
66 | 99 | %endif |
|
67 | 100 | <p class="register_message"> |
|
68 |
${ |
|
|
101 | ${register_message|n} | |
|
69 | 102 | </p> |
|
70 | 103 | |
|
71 | 104 | ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")} |
|
72 | 105 | |
|
73 | 106 | ${h.end_form()} |
|
74 | 107 | </div> |
|
75 | ||
|
76 | % if c.social_plugins: | |
|
77 | <p>${_('Register using one of external services')}:</p> | |
|
78 | ||
|
79 | <p> | |
|
80 | ${render_social_buttons(c.social_plugins, 'register')} | |
|
81 | </p> | |
|
82 | % endif | |
|
83 | ||
|
108 | <%block name="below_register_button" /> | |
|
84 | 109 | </div> |
|
85 | 110 | </div> |
|
86 | 111 | </div> |
|
87 | 112 | |
|
88 |
%if c |
|
|
113 | %if captcha_active: | |
|
89 | 114 | <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> |
|
90 | 115 | %endif |
|
91 | 116 | <script type="text/javascript"> |
|
92 | 117 | $(document).ready(function(){ |
|
93 | 118 | $('#username').focus(); |
|
94 |
%if |
|
|
95 |
Recaptcha.create("${c |
|
|
119 | %if captcha_active: | |
|
120 | Recaptcha.create("${captcha_public_key}", "recaptcha", {theme: "white"}); | |
|
96 | 121 | %endif |
|
97 | 122 | }); |
|
98 | 123 | </script> |
@@ -6,7 +6,13 b'' | |||
|
6 | 6 | <th>${_('Commit')}</th> |
|
7 | 7 | <th></th> |
|
8 | 8 | <th>${_('Commit message')}</th> |
|
9 |
<th> |
|
|
9 | <th> | |
|
10 | %if c.sort == 'newfirst': | |
|
11 | <a href="${c.url_generator(sort='oldfirst')}">${_('Age (new first)')}</a> | |
|
12 | %else: | |
|
13 | <a href="${c.url_generator(sort='newfirst')}">${_('Age (old first)')}</a> | |
|
14 | %endif | |
|
15 | </th> | |
|
10 | 16 | <th>${_('Author')}</th> |
|
11 | 17 | </tr> |
|
12 | 18 | %for entry in c.formatted_results: |
@@ -33,14 +39,14 b'' | |||
|
33 | 39 | </div> |
|
34 | 40 | </td> |
|
35 | 41 | <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open"> |
|
36 |
%if entry |
|
|
42 | %if entry.get('message_hl'): | |
|
37 | 43 | ${h.literal(entry['message_hl'])} |
|
38 | 44 | %else: |
|
39 | 45 | ${h.urlify_commit_message(entry['message'], entry['repository'])} |
|
40 | 46 | %endif |
|
41 | 47 | </td> |
|
42 | 48 | <td class="td-time"> |
|
43 | ${h.age_component(h.time_to_datetime(entry['date']))} | |
|
49 | ${h.age_component(h.time_to_utcdatetime(entry['date']))} | |
|
44 | 50 | </td> |
|
45 | 51 | |
|
46 | 52 | <td class="td-user author"> |
@@ -1,3 +1,40 b'' | |||
|
1 | <%def name="highlight_text_file(terms, text, url, line_context=3, | |
|
2 | max_lines=10, | |
|
3 | mimetype=None, filepath=None)"> | |
|
4 | <% | |
|
5 | lines = text.split('\n') | |
|
6 | lines_of_interest = set() | |
|
7 | matching_lines = h.get_matching_line_offsets(lines, terms) | |
|
8 | shown_matching_lines = 0 | |
|
9 | ||
|
10 | for line_number in matching_lines: | |
|
11 | if len(lines_of_interest) < max_lines: | |
|
12 | lines_of_interest |= set(range( | |
|
13 | max(line_number - line_context, 0), | |
|
14 | min(line_number + line_context, len(lines) + 1))) | |
|
15 | shown_matching_lines += 1 | |
|
16 | ||
|
17 | %> | |
|
18 | ${h.code_highlight( | |
|
19 | text, | |
|
20 | h.get_lexer_safe( | |
|
21 | mimetype=mimetype, | |
|
22 | filepath=filepath, | |
|
23 | ), | |
|
24 | h.SearchContentCodeHtmlFormatter( | |
|
25 | linenos=True, | |
|
26 | cssclass="code-highlight", | |
|
27 | url=url, | |
|
28 | query_terms=terms, | |
|
29 | only_line_numbers=lines_of_interest | |
|
30 | ))|n} | |
|
31 | %if len(matching_lines) > shown_matching_lines: | |
|
32 | <a href="${url}"> | |
|
33 | ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')} | |
|
34 | </p> | |
|
35 | %endif | |
|
36 | </%def> | |
|
37 | ||
|
1 | 38 | <div class="search-results"> |
|
2 | 39 | %for entry in c.formatted_results: |
|
3 | 40 | ## search results are additionally filtered, and this check is just a safe gate |
@@ -29,7 +66,7 b'' | |||
|
29 | 66 | <div class="buttons"> |
|
30 | 67 | <a id="file_history_overview_full" href="${h.url('changelog_file_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}"> |
|
31 | 68 | ${_('Show Full History')} |
|
32 |
</a> | |
|
|
69 | </a> | | |
|
33 | 70 | ${h.link_to(_('Annotation'), h.url('files_annotate_home', repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))} |
|
34 | 71 | | ${h.link_to(_('Raw'), h.url('files_raw_home', repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))} |
|
35 | 72 | | <a href="${h.url('files_rawfile_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}"> |
@@ -38,8 +75,10 b'' | |||
|
38 | 75 | </div> |
|
39 | 76 | </div> |
|
40 | 77 | <div class="code-body search-code-body"> |
|
41 | <pre>${h.literal(entry['content_short_hl'])}</pre> | |
|
42 | </div> | |
|
78 | ${highlight_text_file(c.cur_query, entry['content'], | |
|
79 | url=h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']), | |
|
80 | mimetype=entry.get('mimetype'), filepath=entry.get('path'))} | |
|
81 | </div> | |
|
43 | 82 | </div> |
|
44 | 83 | % endif |
|
45 | 84 | %endfor |
@@ -49,3 +88,14 b'' | |||
|
49 | 88 | ${c.formatted_results.pager('$link_previous ~2~ $link_next')} |
|
50 | 89 | </div> |
|
51 | 90 | %endif |
|
91 | ||
|
92 | %if c.cur_query: | |
|
93 | <script type="text/javascript"> | |
|
94 | $(function(){ | |
|
95 | $(".code").mark( | |
|
96 | '${' '.join(h.normalize_text_for_matching(c.cur_query).split())}', | |
|
97 | {"className": 'match', | |
|
98 | }); | |
|
99 | }) | |
|
100 | </script> | |
|
101 | %endif No newline at end of file |
@@ -43,11 +43,13 b' from nose.plugins.skip import SkipTest' | |||
|
43 | 43 | import pytest |
|
44 | 44 | |
|
45 | 45 | from rhodecode import is_windows |
|
46 | from rhodecode.config.routing import ADMIN_PREFIX | |
|
46 | 47 | from rhodecode.model.meta import Session |
|
47 | 48 | from rhodecode.model.db import User |
|
48 | 49 | from rhodecode.lib import auth |
|
49 | 50 | from rhodecode.lib.helpers import flash, link_to |
|
50 | 51 | from rhodecode.lib.utils2 import safe_unicode, safe_str |
|
52 | from rhodecode.tests.utils import get_session_from_response | |
|
51 | 53 | |
|
52 | 54 | # TODO: johbo: Solve time zone related issues and remove this tweak |
|
53 | 55 | os.environ['TZ'] = 'UTC' |
@@ -177,26 +179,29 b' class TestController(object):' | |||
|
177 | 179 | |
|
178 | 180 | def login_user_session( |
|
179 | 181 | app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS): |
|
180 | response = app.post(url(controller='login', action='index'), | |
|
181 | {'username': username, | |
|
182 | 'password': password}) | |
|
183 | ||
|
182 | from rhodecode.tests.functional.test_login import login_url | |
|
183 | response = app.post( | |
|
184 | login_url, | |
|
185 | {'username': username, 'password': password}) | |
|
184 | 186 | if 'invalid user name' in response.body: |
|
185 | 187 | pytest.fail('could not login using %s %s' % (username, password)) |
|
186 | 188 | |
|
187 | 189 | assert response.status == '302 Found' |
|
188 | ses = response.session['rhodecode_user'] | |
|
189 | assert ses.get('username') == username | |
|
190 | 190 | response = response.follow() |
|
191 | assert ses.get('is_authenticated') | |
|
191 | assert response.status == '200 OK' | |
|
192 | 192 | |
|
193 | return response.session | |
|
193 | session = get_session_from_response(response) | |
|
194 | assert 'rhodecode_user' in session | |
|
195 | rc_user = session['rhodecode_user'] | |
|
196 | assert rc_user.get('username') == username | |
|
197 | assert rc_user.get('is_authenticated') | |
|
198 | ||
|
199 | return session | |
|
194 | 200 | |
|
195 | 201 | |
|
196 | 202 | def logout_user_session(app, csrf_token): |
|
197 | app.post( | |
|
198 | url(controller='login', action='logout'), | |
|
199 | {'csrf_token': csrf_token}, status=302) | |
|
203 | from rhodecode.tests.functional.test_login import logut_url | |
|
204 | app.post(logut_url, {'csrf_token': csrf_token}, status=302) | |
|
200 | 205 | |
|
201 | 206 | |
|
202 | 207 | def login_user(app, username=TEST_USER_ADMIN_LOGIN, |
@@ -20,7 +20,8 b'' | |||
|
20 | 20 | |
|
21 | 21 | import pytest |
|
22 | 22 | |
|
23 |
from rhodecode.tests import assert_session_flash |
|
|
23 | from rhodecode.tests import assert_session_flash | |
|
24 | from rhodecode.tests.utils import AssertResponse | |
|
24 | 25 | from rhodecode.model.db import Session |
|
25 | 26 | from rhodecode.model.settings import SettingsModel |
|
26 | 27 | |
@@ -150,12 +151,14 b' class TestAuthSettingsController(object)' | |||
|
150 | 151 | 'egg:rhodecode-enterprise-ce#rhodecode,' |
|
151 | 152 | 'egg:rhodecode-enterprise-ce#ldap', |
|
152 | 153 | csrf_token) |
|
154 | invalid_port_value = 'invalid-port-number' | |
|
153 | 155 | response = self._post_ldap_settings(params, override={ |
|
154 |
'port': |
|
|
156 | 'port': invalid_port_value, | |
|
155 | 157 | }) |
|
156 | response.mustcontain( | |
|
157 | '<span class="error-message">"invalid-port-number"' | |
|
158 | ' is not a number</span>') | |
|
158 | assertr = AssertResponse(response) | |
|
159 | assertr.element_contains( | |
|
160 | '.form .field #port ~ .error-message', | |
|
161 | invalid_port_value) | |
|
159 | 162 | |
|
160 | 163 | def test_ldap_error_form(self, csrf_token): |
|
161 | 164 | params = self._enable_plugins( |
@@ -339,53 +339,3 b' class TestMyAccountController(TestContro' | |||
|
339 | 339 | new_password_hash = response.session['rhodecode_user']['password'] |
|
340 | 340 | |
|
341 | 341 | assert old_password_hash != new_password_hash |
|
342 | ||
|
343 | def test_my_account_oauth_tokens_empty(self): | |
|
344 | usr = self.log_user('test_regular2', 'test12') | |
|
345 | User.get(usr['user_id']) | |
|
346 | response = self.app.get(url('my_account_oauth')) | |
|
347 | response.mustcontain(no=['Connect with GitHub']) | |
|
348 | response.mustcontain('You have no accounts linked yet') | |
|
349 | ||
|
350 | def test_my_account_oauth_tokens_present(self): | |
|
351 | from rhodecode.model.db import ExternalIdentity | |
|
352 | usr = self.log_user('test_regular2', 'test12') | |
|
353 | user = User.get(usr['user_id']) | |
|
354 | ||
|
355 | ex_identity = ExternalIdentity() | |
|
356 | ex_identity.external_id = '55' | |
|
357 | ex_identity.provider_name = 'twitter' | |
|
358 | ex_identity.local_user_id = user.user_id | |
|
359 | db_session = Session() | |
|
360 | db_session.add(ex_identity) | |
|
361 | Session.flush() | |
|
362 | db_session.commit() | |
|
363 | try: | |
|
364 | response = self.app.get(url('my_account_oauth')) | |
|
365 | response.mustcontain('twitter', | |
|
366 | no=['You have no accounts linked yet']) | |
|
367 | finally: | |
|
368 | db_session = Session() | |
|
369 | db_session.delete(ex_identity) | |
|
370 | db_session.commit() | |
|
371 | ||
|
372 | def test_my_account_oauth_tokens_delete(self): | |
|
373 | from rhodecode.model.db import ExternalIdentity | |
|
374 | usr = self.log_user('test_regular2', 'test12') | |
|
375 | user = User.get(usr['user_id']) | |
|
376 | ||
|
377 | ex_identity = ExternalIdentity() | |
|
378 | ex_identity.external_id = '99' | |
|
379 | ex_identity.provider_name = 'twitter' | |
|
380 | ex_identity.local_user_id = user.user_id | |
|
381 | db_session = Session() | |
|
382 | db_session.add(ex_identity) | |
|
383 | Session.flush() | |
|
384 | db_session.commit() | |
|
385 | assert ExternalIdentity.query().count() == 1 | |
|
386 | response = self.app.post( | |
|
387 | url('my_account_oauth', provider_name='twitter', | |
|
388 | external_id='99'), | |
|
389 | {'_method': 'delete', 'csrf_token': self.csrf_token}) | |
|
390 | assert_session_flash(response, 'OAuth token successfully deleted') | |
|
391 | assert ExternalIdentity.query().count() == 0 |
@@ -22,6 +22,7 b' import mock' | |||
|
22 | 22 | import pytest |
|
23 | 23 | |
|
24 | 24 | import rhodecode |
|
25 | from rhodecode.config.routing import ADMIN_PREFIX | |
|
25 | 26 | from rhodecode.lib.utils2 import md5 |
|
26 | 27 | from rhodecode.model.db import RhodeCodeUi |
|
27 | 28 | from rhodecode.model.meta import Session |
@@ -157,7 +158,7 b' class TestAdminSettingsGlobal:' | |||
|
157 | 158 | 'csrf_token': csrf_token, |
|
158 | 159 | }) |
|
159 | 160 | |
|
160 |
response = self.app.get( |
|
|
161 | response = self.app.get(ADMIN_PREFIX + '/register') | |
|
161 | 162 | response.mustcontain('captcha') |
|
162 | 163 | |
|
163 | 164 | def test_captcha_deactivate(self, csrf_token): |
@@ -167,7 +168,7 b' class TestAdminSettingsGlobal:' | |||
|
167 | 168 | 'csrf_token': csrf_token, |
|
168 | 169 | }) |
|
169 | 170 | |
|
170 |
response = self.app.get( |
|
|
171 | response = self.app.get(ADMIN_PREFIX + '/register') | |
|
171 | 172 | response.mustcontain(no=['captcha']) |
|
172 | 173 | |
|
173 | 174 | def test_title_change(self, csrf_token): |
@@ -35,7 +35,8 b' class TestAdminUsersGroupsController(Tes' | |||
|
35 | 35 | |
|
36 | 36 | def test_index(self): |
|
37 | 37 | self.log_user() |
|
38 | self.app.get(url('users_groups')) | |
|
38 | response = self.app.get(url('users_groups')) | |
|
39 | response.status_int == 200 | |
|
39 | 40 | |
|
40 | 41 | def test_create(self): |
|
41 | 42 | self.log_user() |
@@ -148,7 +149,19 b' class TestAdminUsersGroupsController(Tes' | |||
|
148 | 149 | fixture.destroy_user_group(users_group_name) |
|
149 | 150 | |
|
150 | 151 | def test_edit(self): |
|
151 | self.app.get(url('edit_users_group', user_group_id=1)) | |
|
152 | self.log_user() | |
|
153 | ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True) | |
|
154 | response = self.app.get( | |
|
155 | url('edit_users_group', user_group_id=ug.users_group_id)) | |
|
156 | fixture.destroy_user_group(TEST_USER_GROUP) | |
|
157 | ||
|
158 | def test_edit_user_group_members(self): | |
|
159 | self.log_user() | |
|
160 | ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True) | |
|
161 | response = self.app.get( | |
|
162 | url('edit_user_group_members', user_group_id=ug.users_group_id)) | |
|
163 | response.mustcontain('No members yet') | |
|
164 | fixture.destroy_user_group(TEST_USER_GROUP) | |
|
152 | 165 | |
|
153 | 166 | def test_usergroup_escape(self): |
|
154 | 167 | user = User.get_by_username('test_admin') |
@@ -77,7 +77,7 b' class TestCompareController:' | |||
|
77 | 77 | 'hg': { |
|
78 | 78 | 'tag': 'v0.2.0', |
|
79 | 79 | 'branch': 'default', |
|
80 |
'response': (147, 570 |
|
|
80 | 'response': (147, 5701, 10177) | |
|
81 | 81 | }, |
|
82 | 82 | 'git': { |
|
83 | 83 | 'tag': 'v0.2.2', |
@@ -181,19 +181,25 b' class TestUserAutocompleteData(TestContr' | |||
|
181 | 181 | def assert_and_get_content(result): |
|
182 | 182 | repos = [] |
|
183 | 183 | groups = [] |
|
184 | commits = [] | |
|
184 | 185 | for data in result: |
|
185 | 186 | for data_item in data['children']: |
|
186 | 187 | assert data_item['id'] |
|
187 | 188 | assert data_item['text'] |
|
189 | assert data_item['url'] | |
|
188 | 190 | if data_item['type'] == 'repo': |
|
189 | 191 | repos.append(data_item) |
|
190 | else: | |
|
192 | elif data_item['type'] == 'group': | |
|
191 | 193 | groups.append(data_item) |
|
194 | elif data_item['type'] == 'commit': | |
|
195 | commits.append(data_item) | |
|
196 | else: | |
|
197 | raise Exception('invalid type %s' % data_item['type']) | |
|
192 | 198 | |
|
193 | return repos, groups | |
|
199 | return repos, groups, commits | |
|
194 | 200 | |
|
195 | 201 | |
|
196 |
class Test |
|
|
202 | class TestGotoSwitcherData(TestController): | |
|
197 | 203 | required_repos_with_groups = [ |
|
198 | 204 | 'abc', |
|
199 | 205 | 'abc-fork', |
@@ -253,39 +259,41 b' class TestRepoSwitcherData(TestControlle' | |||
|
253 | 259 | self.log_user() |
|
254 | 260 | |
|
255 | 261 | response = self.app.get( |
|
256 |
url(controller='home', action=' |
|
|
262 | url(controller='home', action='goto_switcher_data'), | |
|
257 | 263 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
258 | 264 | result = json.loads(response.body)['results'] |
|
259 | 265 | |
|
260 | repos, groups = assert_and_get_content(result) | |
|
266 | repos, groups, commits = assert_and_get_content(result) | |
|
261 | 267 | |
|
262 | 268 | assert len(repos) == len(Repository.get_all()) |
|
263 | 269 | assert len(groups) == len(RepoGroup.get_all()) |
|
270 | assert len(commits) == 0 | |
|
264 | 271 | |
|
265 | 272 | def test_returns_list_of_repos_and_groups_filtered(self): |
|
266 | 273 | self.log_user() |
|
267 | 274 | |
|
268 | 275 | response = self.app.get( |
|
269 |
url(controller='home', action=' |
|
|
276 | url(controller='home', action='goto_switcher_data'), | |
|
270 | 277 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
271 | 278 | params={'query': 'abc'}, status=200) |
|
272 | 279 | result = json.loads(response.body)['results'] |
|
273 | 280 | |
|
274 | repos, groups = assert_and_get_content(result) | |
|
281 | repos, groups, commits = assert_and_get_content(result) | |
|
275 | 282 | |
|
276 | 283 | assert len(repos) == 13 |
|
277 | 284 | assert len(groups) == 5 |
|
285 | assert len(commits) == 0 | |
|
278 | 286 | |
|
279 | 287 | def test_returns_list_of_properly_sorted_and_filtered(self): |
|
280 | 288 | self.log_user() |
|
281 | 289 | |
|
282 | 290 | response = self.app.get( |
|
283 |
url(controller='home', action=' |
|
|
291 | url(controller='home', action='goto_switcher_data'), | |
|
284 | 292 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
285 | 293 | params={'query': 'abc'}, status=200) |
|
286 | 294 | result = json.loads(response.body)['results'] |
|
287 | 295 | |
|
288 | repos, groups = assert_and_get_content(result) | |
|
296 | repos, groups, commits = assert_and_get_content(result) | |
|
289 | 297 | |
|
290 | 298 | test_repos = [x['text'] for x in repos[:4]] |
|
291 | 299 | assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos |
@@ -300,54 +308,58 b' class TestRepoListData(TestController):' | |||
|
300 | 308 | self.log_user() |
|
301 | 309 | |
|
302 | 310 | response = self.app.get( |
|
303 |
url(controller='home', action='repo_ |
|
|
311 | url(controller='home', action='repo_list_data'), | |
|
304 | 312 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) |
|
305 | 313 | result = json.loads(response.body)['results'] |
|
306 | 314 | |
|
307 | repos, groups = assert_and_get_content(result) | |
|
315 | repos, groups, commits = assert_and_get_content(result) | |
|
308 | 316 | |
|
309 | 317 | assert len(repos) == len(Repository.get_all()) |
|
310 | 318 | assert len(groups) == 0 |
|
319 | assert len(commits) == 0 | |
|
311 | 320 | |
|
312 | 321 | def test_returns_list_of_repos_and_groups_filtered(self): |
|
313 | 322 | self.log_user() |
|
314 | 323 | |
|
315 | 324 | response = self.app.get( |
|
316 |
url(controller='home', action='repo_ |
|
|
325 | url(controller='home', action='repo_list_data'), | |
|
317 | 326 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
318 | 327 | params={'query': 'vcs_test_git'}, status=200) |
|
319 | 328 | result = json.loads(response.body)['results'] |
|
320 | 329 | |
|
321 | repos, groups = assert_and_get_content(result) | |
|
330 | repos, groups, commits = assert_and_get_content(result) | |
|
322 | 331 | |
|
323 | 332 | assert len(repos) == len(Repository.query().filter( |
|
324 | 333 | Repository.repo_name.ilike('%vcs_test_git%')).all()) |
|
325 | 334 | assert len(groups) == 0 |
|
335 | assert len(commits) == 0 | |
|
326 | 336 | |
|
327 | 337 | def test_returns_list_of_repos_and_groups_filtered_with_type(self): |
|
328 | 338 | self.log_user() |
|
329 | 339 | |
|
330 | 340 | response = self.app.get( |
|
331 |
url(controller='home', action='repo_ |
|
|
341 | url(controller='home', action='repo_list_data'), | |
|
332 | 342 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
333 | 343 | params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200) |
|
334 | 344 | result = json.loads(response.body)['results'] |
|
335 | 345 | |
|
336 | repos, groups = assert_and_get_content(result) | |
|
346 | repos, groups, commits = assert_and_get_content(result) | |
|
337 | 347 | |
|
338 | 348 | assert len(repos) == len(Repository.query().filter( |
|
339 | 349 | Repository.repo_name.ilike('%vcs_test_git%')).all()) |
|
340 | 350 | assert len(groups) == 0 |
|
351 | assert len(commits) == 0 | |
|
341 | 352 | |
|
342 | 353 | def test_returns_list_of_repos_non_ascii_query(self): |
|
343 | 354 | self.log_user() |
|
344 | 355 | response = self.app.get( |
|
345 |
url(controller='home', action='repo_ |
|
|
356 | url(controller='home', action='repo_list_data'), | |
|
346 | 357 | headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, |
|
347 | 358 | params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200) |
|
348 | 359 | result = json.loads(response.body)['results'] |
|
349 | 360 | |
|
350 | repos, groups = assert_and_get_content(result) | |
|
361 | repos, groups, commits = assert_and_get_content(result) | |
|
351 | 362 | |
|
352 | 363 | assert len(repos) == 0 |
|
353 | 364 | assert len(groups) == 0 |
|
365 | assert len(commits) == 0 |
@@ -23,9 +23,11 b' import urlparse' | |||
|
23 | 23 | import mock |
|
24 | 24 | import pytest |
|
25 | 25 | |
|
26 | from rhodecode.config.routing import ADMIN_PREFIX | |
|
26 | 27 | from rhodecode.tests import ( |
|
27 | 28 | assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN) |
|
28 | 29 | from rhodecode.tests.fixture import Fixture |
|
30 | from rhodecode.tests.utils import AssertResponse, get_session_from_response | |
|
29 | 31 | from rhodecode.lib.auth import check_password, generate_auth_token |
|
30 | 32 | from rhodecode.lib import helpers as h |
|
31 | 33 | from rhodecode.model.auth_token import AuthTokenModel |
@@ -35,6 +37,14 b' from rhodecode.model.meta import Session' | |||
|
35 | 37 | |
|
36 | 38 | fixture = Fixture() |
|
37 | 39 | |
|
40 | # Hardcode URLs because we don't have a request object to use | |
|
41 | # pyramids URL generation methods. | |
|
42 | login_url = ADMIN_PREFIX + '/login' | |
|
43 | logut_url = ADMIN_PREFIX + '/logout' | |
|
44 | register_url = ADMIN_PREFIX + '/register' | |
|
45 | pwd_reset_url = ADMIN_PREFIX + '/password_reset' | |
|
46 | pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation' | |
|
47 | ||
|
38 | 48 | |
|
39 | 49 | @pytest.mark.usefixtures('app') |
|
40 | 50 | class TestLoginController: |
@@ -52,37 +62,38 b' class TestLoginController:' | |||
|
52 | 62 | assert Notification.query().all() == [] |
|
53 | 63 | |
|
54 | 64 | def test_index(self): |
|
55 |
response = self.app.get( |
|
|
65 | response = self.app.get(login_url) | |
|
56 | 66 | assert response.status == '200 OK' |
|
57 | 67 | # Test response... |
|
58 | 68 | |
|
59 | 69 | def test_login_admin_ok(self): |
|
60 |
response = self.app.post( |
|
|
70 | response = self.app.post(login_url, | |
|
61 | 71 | {'username': 'test_admin', |
|
62 | 72 | 'password': 'test12'}) |
|
63 | 73 | assert response.status == '302 Found' |
|
64 | username = response.session['rhodecode_user'].get('username') | |
|
74 | session = get_session_from_response(response) | |
|
75 | username = session['rhodecode_user'].get('username') | |
|
65 | 76 | assert username == 'test_admin' |
|
66 | 77 | response = response.follow() |
|
67 | 78 | response.mustcontain('/%s' % HG_REPO) |
|
68 | 79 | |
|
69 | 80 | def test_login_regular_ok(self): |
|
70 |
response = self.app.post( |
|
|
81 | response = self.app.post(login_url, | |
|
71 | 82 | {'username': 'test_regular', |
|
72 | 83 | 'password': 'test12'}) |
|
73 | 84 | |
|
74 | 85 | assert response.status == '302 Found' |
|
75 | username = response.session['rhodecode_user'].get('username') | |
|
86 | session = get_session_from_response(response) | |
|
87 | username = session['rhodecode_user'].get('username') | |
|
76 | 88 | assert username == 'test_regular' |
|
77 | 89 | response = response.follow() |
|
78 | 90 | response.mustcontain('/%s' % HG_REPO) |
|
79 | 91 | |
|
80 | 92 | def test_login_ok_came_from(self): |
|
81 | 93 | test_came_from = '/_admin/users?branch=stable' |
|
82 | response = self.app.post(url(controller='login', action='index', | |
|
83 | came_from=test_came_from), | |
|
84 |
|
|
|
85 | 'password': 'test12'}) | |
|
94 | _url = '{}?came_from={}'.format(login_url, test_came_from) | |
|
95 | response = self.app.post( | |
|
96 | _url, {'username': 'test_admin', 'password': 'test12'}) | |
|
86 | 97 | assert response.status == '302 Found' |
|
87 | 98 | assert 'branch=stable' in response.location |
|
88 | 99 | response = response.follow() |
@@ -100,33 +111,30 b' class TestLoginController:' | |||
|
100 | 111 | assert 'branch=stable' in response_query[0][1] |
|
101 | 112 | |
|
102 | 113 | def test_login_form_with_get_args(self): |
|
103 | kwargs = {'branch': 'stable'} | |
|
104 | response = self.app.get( | |
|
105 | url(controller='login', action='index', | |
|
106 | came_from='/_admin/users', **kwargs)) | |
|
107 | assert 'branch=stable' in response.form.action | |
|
114 | _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url) | |
|
115 | response = self.app.get(_url) | |
|
116 | assert 'branch%3Dstable' in response.form.action | |
|
108 | 117 | |
|
109 | 118 | @pytest.mark.parametrize("url_came_from", [ |
|
110 |
|
|
|
111 |
|
|
|
112 |
|
|
|
113 |
|
|
|
114 |
|
|
|
115 |
|
|
|
119 | 'data:text/html,<script>window.alert("xss")</script>', | |
|
120 | 'mailto:test@rhodecode.org', | |
|
121 | 'file:///etc/passwd', | |
|
122 | 'ftp://some.ftp.server', | |
|
123 | 'http://other.domain', | |
|
124 | '/\r\nX-Forwarded-Host: http://example.org', | |
|
116 | 125 | ]) |
|
117 | 126 | def test_login_bad_came_froms(self, url_came_from): |
|
118 | response = self.app.post(url(controller='login', action='index', | |
|
119 | came_from=url_came_from), | |
|
120 | {'username': 'test_admin', | |
|
121 |
|
|
|
127 | _url = '{}?came_from={}'.format(login_url, url_came_from) | |
|
128 | response = self.app.post( | |
|
129 | _url, | |
|
130 | {'username': 'test_admin', 'password': 'test12'}) | |
|
122 | 131 | assert response.status == '302 Found' |
|
123 | assert response.tmpl_context.came_from == '/' | |
|
124 | ||
|
125 | 132 | response = response.follow() |
|
126 | 133 | assert response.status == '200 OK' |
|
134 | assert response.request.path == '/' | |
|
127 | 135 | |
|
128 | 136 | def test_login_short_password(self): |
|
129 |
response = self.app.post( |
|
|
137 | response = self.app.post(login_url, | |
|
130 | 138 | {'username': 'test_admin', |
|
131 | 139 | 'password': 'as'}) |
|
132 | 140 | assert response.status == '200 OK' |
@@ -135,7 +143,7 b' class TestLoginController:' | |||
|
135 | 143 | |
|
136 | 144 | def test_login_wrong_non_ascii_password(self, user_regular): |
|
137 | 145 | response = self.app.post( |
|
138 | url(controller='login', action='index'), | |
|
146 | login_url, | |
|
139 | 147 | {'username': user_regular.username, |
|
140 | 148 | 'password': u'invalid-non-asci\xe4'.encode('utf8')}) |
|
141 | 149 | |
@@ -146,13 +154,13 b' class TestLoginController:' | |||
|
146 | 154 | password = u'valid-non-ascii\xe4' |
|
147 | 155 | user = user_util.create_user(password=password) |
|
148 | 156 | response = self.app.post( |
|
149 | url(controller='login', action='index'), | |
|
157 | login_url, | |
|
150 | 158 | {'username': user.username, |
|
151 | 159 | 'password': password.encode('utf-8')}) |
|
152 | 160 | assert response.status_code == 302 |
|
153 | 161 | |
|
154 | 162 | def test_login_wrong_username_password(self): |
|
155 |
response = self.app.post( |
|
|
163 | response = self.app.post(login_url, | |
|
156 | 164 | {'username': 'error', |
|
157 | 165 | 'password': 'test12'}) |
|
158 | 166 | |
@@ -170,12 +178,13 b' class TestLoginController:' | |||
|
170 | 178 | Session().add(user) |
|
171 | 179 | Session().commit() |
|
172 | 180 | self.destroy_users.add(temp_user) |
|
173 |
response = self.app.post( |
|
|
181 | response = self.app.post(login_url, | |
|
174 | 182 | {'username': temp_user, |
|
175 | 183 | 'password': 'test123'}) |
|
176 | 184 | |
|
177 | 185 | assert response.status == '302 Found' |
|
178 | username = response.session['rhodecode_user'].get('username') | |
|
186 | session = get_session_from_response(response) | |
|
187 | username = session['rhodecode_user'].get('username') | |
|
179 | 188 | assert username == temp_user |
|
180 | 189 | response = response.follow() |
|
181 | 190 | response.mustcontain('/%s' % HG_REPO) |
@@ -186,13 +195,13 b' class TestLoginController:' | |||
|
186 | 195 | |
|
187 | 196 | # REGISTRATIONS |
|
188 | 197 | def test_register(self): |
|
189 |
response = self.app.get( |
|
|
198 | response = self.app.get(register_url) | |
|
190 | 199 | response.mustcontain('Create an Account') |
|
191 | 200 | |
|
192 | 201 | def test_register_err_same_username(self): |
|
193 | 202 | uname = 'test_admin' |
|
194 | 203 | response = self.app.post( |
|
195 | url(controller='login', action='register'), | |
|
204 | register_url, | |
|
196 | 205 | { |
|
197 | 206 | 'username': uname, |
|
198 | 207 | 'password': 'test12', |
@@ -203,13 +212,14 b' class TestLoginController:' | |||
|
203 | 212 | } |
|
204 | 213 | ) |
|
205 | 214 | |
|
215 | assertr = AssertResponse(response) | |
|
206 | 216 | msg = validators.ValidUsername()._messages['username_exists'] |
|
207 |
msg = |
|
|
208 | response.mustcontain(msg) | |
|
217 | msg = msg % {'username': uname} | |
|
218 | assertr.element_contains('#username+.error-message', msg) | |
|
209 | 219 | |
|
210 | 220 | def test_register_err_same_email(self): |
|
211 | 221 | response = self.app.post( |
|
212 | url(controller='login', action='register'), | |
|
222 | register_url, | |
|
213 | 223 | { |
|
214 | 224 | 'username': 'test_admin_0', |
|
215 | 225 | 'password': 'test12', |
@@ -220,12 +230,13 b' class TestLoginController:' | |||
|
220 | 230 | } |
|
221 | 231 | ) |
|
222 | 232 | |
|
233 | assertr = AssertResponse(response) | |
|
223 | 234 | msg = validators.UniqSystemEmail()()._messages['email_taken'] |
|
224 | response.mustcontain(msg) | |
|
235 | assertr.element_contains('#email+.error-message', msg) | |
|
225 | 236 | |
|
226 | 237 | def test_register_err_same_email_case_sensitive(self): |
|
227 | 238 | response = self.app.post( |
|
228 | url(controller='login', action='register'), | |
|
239 | register_url, | |
|
229 | 240 | { |
|
230 | 241 | 'username': 'test_admin_1', |
|
231 | 242 | 'password': 'test12', |
@@ -235,12 +246,13 b' class TestLoginController:' | |||
|
235 | 246 | 'lastname': 'test' |
|
236 | 247 | } |
|
237 | 248 | ) |
|
249 | assertr = AssertResponse(response) | |
|
238 | 250 | msg = validators.UniqSystemEmail()()._messages['email_taken'] |
|
239 | response.mustcontain(msg) | |
|
251 | assertr.element_contains('#email+.error-message', msg) | |
|
240 | 252 | |
|
241 | 253 | def test_register_err_wrong_data(self): |
|
242 | 254 | response = self.app.post( |
|
243 | url(controller='login', action='register'), | |
|
255 | register_url, | |
|
244 | 256 | { |
|
245 | 257 | 'username': 'xs', |
|
246 | 258 | 'password': 'test', |
@@ -256,7 +268,7 b' class TestLoginController:' | |||
|
256 | 268 | |
|
257 | 269 | def test_register_err_username(self): |
|
258 | 270 | response = self.app.post( |
|
259 | url(controller='login', action='register'), | |
|
271 | register_url, | |
|
260 | 272 | { |
|
261 | 273 | 'username': 'error user', |
|
262 | 274 | 'password': 'test12', |
@@ -277,7 +289,7 b' class TestLoginController:' | |||
|
277 | 289 | def test_register_err_case_sensitive(self): |
|
278 | 290 | usr = 'Test_Admin' |
|
279 | 291 | response = self.app.post( |
|
280 | url(controller='login', action='register'), | |
|
292 | register_url, | |
|
281 | 293 | { |
|
282 | 294 | 'username': usr, |
|
283 | 295 | 'password': 'test12', |
@@ -288,14 +300,14 b' class TestLoginController:' | |||
|
288 | 300 | } |
|
289 | 301 | ) |
|
290 | 302 | |
|
291 | response.mustcontain('An email address must contain a single @') | |
|
303 | assertr = AssertResponse(response) | |
|
292 | 304 | msg = validators.ValidUsername()._messages['username_exists'] |
|
293 |
msg = |
|
|
294 | response.mustcontain(msg) | |
|
305 | msg = msg % {'username': usr} | |
|
306 | assertr.element_contains('#username+.error-message', msg) | |
|
295 | 307 | |
|
296 | 308 | def test_register_special_chars(self): |
|
297 | 309 | response = self.app.post( |
|
298 | url(controller='login', action='register'), | |
|
310 | register_url, | |
|
299 | 311 | { |
|
300 | 312 | 'username': 'xxxaxn', |
|
301 | 313 | 'password': 'ąćźżąśśśś', |
@@ -311,7 +323,7 b' class TestLoginController:' | |||
|
311 | 323 | |
|
312 | 324 | def test_register_password_mismatch(self): |
|
313 | 325 | response = self.app.post( |
|
314 | url(controller='login', action='register'), | |
|
326 | register_url, | |
|
315 | 327 | { |
|
316 | 328 | 'username': 'xs', |
|
317 | 329 | 'password': '123qwe', |
@@ -332,7 +344,7 b' class TestLoginController:' | |||
|
332 | 344 | lastname = 'testlastname' |
|
333 | 345 | |
|
334 | 346 | response = self.app.post( |
|
335 | url(controller='login', action='register'), | |
|
347 | register_url, | |
|
336 | 348 | { |
|
337 | 349 | 'username': username, |
|
338 | 350 | 'password': password, |
@@ -360,7 +372,7 b' class TestLoginController:' | |||
|
360 | 372 | def test_forgot_password_wrong_mail(self): |
|
361 | 373 | bad_email = 'marcin@wrongmail.org' |
|
362 | 374 | response = self.app.post( |
|
363 | url(controller='login', action='password_reset'), | |
|
375 | pwd_reset_url, | |
|
364 | 376 | {'email': bad_email, } |
|
365 | 377 | ) |
|
366 | 378 | |
@@ -369,8 +381,7 b' class TestLoginController:' | |||
|
369 | 381 | response.mustcontain() |
|
370 | 382 | |
|
371 | 383 | def test_forgot_password(self): |
|
372 |
response = self.app.get( |
|
|
373 | action='password_reset')) | |
|
384 | response = self.app.get(pwd_reset_url) | |
|
374 | 385 | assert response.status == '200 OK' |
|
375 | 386 | |
|
376 | 387 | username = 'test_password_reset_1' |
@@ -389,8 +400,7 b' class TestLoginController:' | |||
|
389 | 400 | Session().add(new) |
|
390 | 401 | Session().commit() |
|
391 | 402 | |
|
392 |
response = self.app.post( |
|
|
393 | action='password_reset'), | |
|
403 | response = self.app.post(pwd_reset_url, | |
|
394 | 404 | {'email': email, }) |
|
395 | 405 | |
|
396 | 406 | assert_session_flash( |
@@ -401,20 +411,18 b' class TestLoginController:' | |||
|
401 | 411 | # BAD KEY |
|
402 | 412 | |
|
403 | 413 | key = "bad" |
|
404 | response = self.app.get(url(controller='login', | |
|
405 | action='password_reset_confirmation', | |
|
406 | key=key)) | |
|
414 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key) | |
|
415 | response = self.app.get(confirm_url) | |
|
407 | 416 | assert response.status == '302 Found' |
|
408 |
assert response.location.endswith( |
|
|
417 | assert response.location.endswith(pwd_reset_url) | |
|
409 | 418 | |
|
410 | 419 | # GOOD KEY |
|
411 | 420 | |
|
412 | 421 | key = User.get_by_username(username).api_key |
|
413 | response = self.app.get(url(controller='login', | |
|
414 | action='password_reset_confirmation', | |
|
415 | key=key)) | |
|
422 | confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key) | |
|
423 | response = self.app.get(confirm_url) | |
|
416 | 424 | assert response.status == '302 Found' |
|
417 |
assert response.location.endswith( |
|
|
425 | assert response.location.endswith(login_url) | |
|
418 | 426 | |
|
419 | 427 | assert_session_flash( |
|
420 | 428 | response, |
@@ -99,12 +99,13 b' class TestPullrequestsController:' | |||
|
99 | 99 | in response) != pr_merge_enabled |
|
100 | 100 | |
|
101 | 101 | def test_close_status_visibility(self, pr_util, csrf_token): |
|
102 | from rhodecode.tests.functional.test_login import login_url, logut_url | |
|
102 | 103 | # Logout |
|
103 | 104 | response = self.app.post( |
|
104 | url(controller='login', action='logout'), | |
|
105 | logut_url, | |
|
105 | 106 | params={'csrf_token': csrf_token}) |
|
106 | 107 | # Login as regular user |
|
107 |
response = self.app.post( |
|
|
108 | response = self.app.post(login_url, | |
|
108 | 109 | {'username': 'test_regular', |
|
109 | 110 | 'password': 'test12'}) |
|
110 | 111 |
@@ -129,6 +129,10 b' class TestSearchController(TestControlle' | |||
|
129 | 129 | ('author:marcin@python-blog.com ' |
|
130 | 130 | 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ |
|
131 | 131 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), |
|
132 | ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ | |
|
133 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |
|
134 | ('b986218b', 1, [ | |
|
135 | ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), | |
|
132 | 136 | ]) |
|
133 | 137 | def test_search_commit_messages( |
|
134 | 138 | self, query, expected_hits, expected_commits, enabled_backends): |
@@ -155,3 +155,29 b' class TestRhodeCodeAuthPlugin(object):' | |||
|
155 | 155 | self.password_generator_mock = password_generator_patch.start() |
|
156 | 156 | self.password_generator_mock.return_value = 'new-password' |
|
157 | 157 | self.finalizers.append(password_generator_patch.stop) |
|
158 | ||
|
159 | ||
|
160 | def test_missing_ldap(): | |
|
161 | from rhodecode.model.validators import Missing | |
|
162 | ||
|
163 | try: | |
|
164 | import ldap_not_existing | |
|
165 | except ImportError: | |
|
166 | # means that python-ldap is not installed | |
|
167 | ldap_not_existing = Missing | |
|
168 | ||
|
169 | # missing is singleton | |
|
170 | assert ldap_not_existing == Missing | |
|
171 | ||
|
172 | ||
|
173 | def test_import_ldap(): | |
|
174 | from rhodecode.model.validators import Missing | |
|
175 | ||
|
176 | try: | |
|
177 | import ldap | |
|
178 | except ImportError: | |
|
179 | # means that python-ldap is not installed | |
|
180 | ldap = Missing | |
|
181 | ||
|
182 | # missing is singleton | |
|
183 | assert False is (ldap == Missing) |
@@ -32,6 +32,36 b' from rhodecode.model.user import UserMod' | |||
|
32 | 32 | from rhodecode.model.user_group import UserGroupModel |
|
33 | 33 | |
|
34 | 34 | |
|
35 | def test_perm_origin_dict(): | |
|
36 | pod = auth.PermOriginDict() | |
|
37 | pod['thing'] = 'read', 'default' | |
|
38 | assert pod['thing'] == 'read' | |
|
39 | ||
|
40 | assert pod.perm_origin_stack == { | |
|
41 | 'thing': [('read', 'default')]} | |
|
42 | ||
|
43 | pod['thing'] = 'write', 'admin' | |
|
44 | assert pod['thing'] == 'write' | |
|
45 | ||
|
46 | assert pod.perm_origin_stack == { | |
|
47 | 'thing': [('read', 'default'), ('write', 'admin')]} | |
|
48 | ||
|
49 | pod['other'] = 'write', 'default' | |
|
50 | ||
|
51 | assert pod.perm_origin_stack == { | |
|
52 | 'other': [('write', 'default')], | |
|
53 | 'thing': [('read', 'default'), ('write', 'admin')]} | |
|
54 | ||
|
55 | pod['other'] = 'none', 'override' | |
|
56 | ||
|
57 | assert pod.perm_origin_stack == { | |
|
58 | 'other': [('write', 'default'), ('none', 'override')], | |
|
59 | 'thing': [('read', 'default'), ('write', 'admin')]} | |
|
60 | ||
|
61 | with pytest.raises(ValueError): | |
|
62 | pod['thing'] = 'read' | |
|
63 | ||
|
64 | ||
|
35 | 65 | def test_cached_perms_data(user_regular, backend_random): |
|
36 | 66 | permissions = get_permissions(user_regular) |
|
37 | 67 | repo_name = backend_random.repo.repo_name |
@@ -155,3 +155,42 b' def test_get_visual_attr(pylonsapp):' | |||
|
155 | 155 | def test_chop_at(test_text, inclusive, expected_text): |
|
156 | 156 | assert helpers.chop_at_smart( |
|
157 | 157 | test_text, '\n', inclusive, '...') == expected_text |
|
158 | ||
|
159 | ||
|
160 | @pytest.mark.parametrize('test_text, expected_output', [ | |
|
161 | ('some text', ['some', 'text']), | |
|
162 | ('some text', ['some', 'text']), | |
|
163 | ('some text "with a phrase"', ['some', 'text', 'with a phrase']), | |
|
164 | ('"a phrase" "another phrase"', ['a phrase', 'another phrase']), | |
|
165 | ('"justphrase"', ['justphrase']), | |
|
166 | ('""', []), | |
|
167 | ('', []), | |
|
168 | (' ', []), | |
|
169 | ('" "', []), | |
|
170 | ]) | |
|
171 | def test_extract_phrases(test_text, expected_output): | |
|
172 | assert helpers.extract_phrases(test_text) == expected_output | |
|
173 | ||
|
174 | ||
|
175 | @pytest.mark.parametrize('test_text, text_phrases, expected_output', [ | |
|
176 | ('some text here', ['some', 'here'], [(0, 4), (10, 14)]), | |
|
177 | ('here here there', ['here'], [(0, 4), (5, 9), (11, 15)]), | |
|
178 | ('irrelevant', ['not found'], []), | |
|
179 | ('irrelevant', ['not found'], []), | |
|
180 | ]) | |
|
181 | def test_get_matching_offsets(test_text, text_phrases, expected_output): | |
|
182 | assert helpers.get_matching_offsets( | |
|
183 | test_text, text_phrases) == expected_output | |
|
184 | ||
|
185 | def test_normalize_text_for_matching(): | |
|
186 | assert helpers.normalize_text_for_matching( | |
|
187 | 'OJjfe)*#$*@)$JF*)3r2f80h') == 'ojjfe jf 3r2f80h' | |
|
188 | ||
|
189 | def test_get_matching_line_offsets(): | |
|
190 | assert helpers.get_matching_line_offsets([ | |
|
191 | 'words words words', | |
|
192 | 'words words words', | |
|
193 | 'some text some', | |
|
194 | 'words words words', | |
|
195 | 'words words words', | |
|
196 | 'text here what'], 'text') == {3: [(5, 9)], 6: [(0, 4)]} No newline at end of file |
@@ -270,3 +270,13 b' def is_url_reachable(url):' | |||
|
270 | 270 | except urllib2.URLError: |
|
271 | 271 | return False |
|
272 | 272 | return True |
|
273 | ||
|
274 | ||
|
275 | def get_session_from_response(response): | |
|
276 | """ | |
|
277 | This returns the session from a response object. Pylons has some magic | |
|
278 | to make the session available as `response.session`. But pyramid | |
|
279 | doesn't expose it. | |
|
280 | """ | |
|
281 | # TODO: Try to look up the session key also. | |
|
282 | return response.request.environ['beaker.session'] |
@@ -214,10 +214,12 b' setup(' | |||
|
214 | 214 | entry_points={ |
|
215 | 215 | 'enterprise.plugins1': [ |
|
216 | 216 | 'crowd=rhodecode.authentication.plugins.auth_crowd:plugin_factory', |
|
217 | 'headers=rhodecode.authentication.plugins.auth_headers:plugin_factory', | |
|
217 | 218 | 'jasig_cas=rhodecode.authentication.plugins.auth_jasig_cas:plugin_factory', |
|
218 | 219 | 'ldap=rhodecode.authentication.plugins.auth_ldap:plugin_factory', |
|
219 | 220 | 'pam=rhodecode.authentication.plugins.auth_pam:plugin_factory', |
|
220 | 221 | 'rhodecode=rhodecode.authentication.plugins.auth_rhodecode:plugin_factory', |
|
222 | 'token=rhodecode.authentication.plugins.auth_token:plugin_factory', | |
|
221 | 223 | ], |
|
222 | 224 | 'paste.app_factory': [ |
|
223 | 225 | 'main=rhodecode.config.middleware:make_pyramid_app', |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed | |
This diff has been collapsed as it changes many lines, (3341 lines changed) Show them Hide them |
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now