##// END OF EJS Templates
Merge branch default into stable
marcink -
r168:40e6b177 merge stable
parent child Browse files
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.0.1
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/pyroutes.js',
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 = 1
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 ## ammount of time a worker can spend with handling a request before it
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 ;appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
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 #use = egg:gunicorn#main
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 #workers = 1
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 #proc_name = rhodecode
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 #worker_class = sync
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 #max_requests = 1000
73 #max_requests_jitter = 30
74 ## ammount of time a worker can spend with handling a request before it
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 #timeout = 21600
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 #beaker.session.lock_dir = %(here)s/data/sessions/lock
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 ;appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
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 = "${builtins.readFile ./rhodecode/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 nginx
13 ## requires static_files = false in .ini file
14 DocumentRoot /path/to/installation/rhodecode/public
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 #important !
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 #rhodecode instance
26 ProxyPass / http://127.0.0.1:5000/
27 ProxyPassReverse / http://127.0.0.1:5000/
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 |RCM| uses `Whoosh`_ to index |repos| and provide full-text search.
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:5000;
10 server 127.0.0.1:10002;
11 11
12 12 # add more instances for load balancing
13 # server 127.0.0.1:5001;
14 # server 127.0.0.1:5002;
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/rhodecode/public;
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 proxy_pass http://rc;
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 |RCM| uses, then you will need to
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 |RCM| instance,
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 |RCM| up and running, run through the below steps:
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 installation you'll be
49 asked for your database credentials, so have them at hand. You don't need
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 on the URL
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: 3.3.0
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: 3.3.0
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 pannels to faster load large ammount of data
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 pyelasticsearch==1.4
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-1.9.0";
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/9b/540e311b31a10c2a904acfb08030c656047e5c7ba479d35df2799e5dccfe/elasticsearch-1.9.0.tar.gz";
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.7";
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; [gnureadline];
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.1";
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.7.1";
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 pyelasticsearch];
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.7.1.zip";
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.7
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.7.1.zip#md5=91daea803aaa264ce7a8213bc2220d4c
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
@@ -1,1 +1,1 b''
1 4.0.1 No newline at end of file
1 4.1.0 No newline at end of file
@@ -47,7 +47,7 b' CONFIG = {}'
47 47 EXTENSIONS = {}
48 48
49 49 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __dbversion__ = 51 # defines current db version for migrations
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': 'http://foo.com/repo'}),
42 ('clone_uri', {'clone_uri': None}),
43 ('landing_rev', {'landing_rev': 'branch:master'}),
44 ('enable_statistics', {'enable_statistics': True}),
45 ('enable_locking', {'enable_locking': True}),
46 ('enable_downloads', {'enable_downloads': True}),
47 ('name', {'name': 'new_repo_name'}),
48 ('repo_group', {'group': 'test_group_for_update'}),
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, changing_attr, updates, backend):
51 repo_name = 'api_update_me'
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 changing_attr == 'repo_group':
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 changing_attr == 'repo_group':
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': repo.get_api_data(include_secrets=True)
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 changing_attr == 'repo_group':
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': repo.get_api_data(include_secrets=True)
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 = 'api_update_me'
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': user_group.get_api_data()
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': user_group.get_api_data()
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 changesets limited by the number of commits starting
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 changesets to this amount
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.scm_instance().get_commits(
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 = 'egg:{}#{}'.format(ep.dist.project_name, ep.name)
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, ExternalIdentity
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. container auth plugin to fetch user by environ params
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 raises exceptions on failure.
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 container auth
626 :param password: password can be empty for container auth
627 :param environ: environ headers passed for container auth
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 container_only = environ and not (username and password)
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 container_only and not plugin.is_container_auth:
655 log.debug('Auth type is for container only and plugin `%s` is not '
656 'container plugin, skipping...', plugin_id)
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 @hybrid_property
96 def is_container_auth(self):
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 ldap = Missing()
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 isinstance(ldap, Missing):
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 form_defaults = {}
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.children:
81 value = self.plugin.get_setting_by_name(node.name) or node.default
82 form_defaults[node.name] = value
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 'Please check the form inputs.'),
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={}, errors=None, prefix_error=False):
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 load_environment
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('repo_switcher_data', '/_repos_and_groups', controller='home',
129 action='repo_switcher_data')
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', '/user_groups/{user_group_id}/edit/global_permissions',
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', '/user_groups/{user_group_id}/edit/global_permissions',
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', '/user_groups/{user_group_id}/edit/permissions_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', '/user_groups/{user_group_id}/edit/permissions',
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', '/user_groups/{user_group_id}/edit/permissions',
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', '/user_groups/{user_group_id}/edit/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', '/user_groups/{user_group_id}/edit/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', '/{repo_name}/changelog/{revision}/{f_path}',
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(url('users_groups'))
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 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
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 repo_switcher_data(self):
197 def goto_switcher_data(self):
167 198 query = request.GET.get('query')
168 log.debug('generating switcher repo/groups list, query %s', query)
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 url('login_home', came_from=came_from))
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(url('login_home', came_from=came_from))
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, AttributeDict, \
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, if not assume UTC
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['app_conf']['search.location']
759 data_path = config['app_conf']['cache_dir']
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_to_timestamp_plus_offset,
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': password_reset_url,
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 user_group.user = User.get_by_username(form_data['user'])
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_python(self, value, state):
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(str(e), value, state)
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: 6em;
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('new_gist', '/_admin/gists/new', []);
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('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
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('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
32 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
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 == "password":
62 <div class="input">
63 ${h.password(node.name, class_="medium")}
64 <p class="help-block">${node.description}</p>
65 </div>
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 </div>
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 }, 1000);
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">${_('Members')}:</label>
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" >${_('Chosen group members')}</label>
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 members')}</label>
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.url('login_home',came_from=h.url.current()), needs_csrf_token=False)}
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.url('reset_password'))}</span>
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.url('register'))}
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.url('logout_home'))}
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 tmpl += '<i class="icon-folder-close"></i> ';
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('repo_switcher_data')}",
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."),h.url('register'))}</h4>
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(h.url.current(**request.GET), needs_csrf_token=False)}
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?'),h.url('reset_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."),h.url('login'))}</h4>
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(url('password_reset'), needs_csrf_token=False)}
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.captcha_active:
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.captcha_active:
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 c.captcha_active:
67 Recaptcha.create("${c.captcha_public_key}", "recaptcha", {theme: "white"});
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."),h.url('login'))}</h4>
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(url('register'), needs_csrf_token= False)}
39 ${h.form(request.route_path('register'), needs_csrf_token=False)}
40
41 41 <label for="username">${_('Username')}:</label>
42 ${h.text('username', c.form_data.get('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', c.form_data.get('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', c.form_data.get('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.captcha_active:
83 %if captcha_active:
55 84 <div>
56 <label for="email">${_('Captcha')}:</label>
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 c.auto_active:
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 ${c.register_message|n}
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.captcha_active:
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 c.captcha_active:
95 Recaptcha.create("${c.captcha_public_key}", "recaptcha", {theme: "white"});
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>${_('Age')}</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['message_hl']:
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, url
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': 'invalid-port-number',
156 'port': invalid_port_value,
155 157 })
156 response.mustcontain(
157 '<span class="error-message">&quot;invalid-port-number&quot;'
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(url('register'))
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(url('register'))
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, 5700, 10176)
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 TestRepoSwitcherData(TestController):
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='repo_switcher_data'),
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='repo_switcher_data'),
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='repo_switcher_data'),
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_switcher_data'),
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_switcher_data'),
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_switcher_data'),
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_switcher_data'),
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(url(controller='login', action='index'))
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(url(controller='login', action='index'),
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(url(controller='login', action='index'),
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 {'username': 'test_admin',
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 ('data:text/html,<script>window.alert("xss")</script>',),
111 ('mailto:test@rhodecode.org',),
112 ('file:///etc/passwd',),
113 ('ftp://some.ftp.server',),
114 ('http://other.domain',),
115 ('/\r\nX-Forwarded-Host: http://example.org',),
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 'password': 'test12'})
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(url(controller='login', action='index'),
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(url(controller='login', action='index'),
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(url(controller='login', action='index'),
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(url(controller='login', action='register'))
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 = h.html_escape(msg % {'username': uname})
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 = h.html_escape(msg % {'username': usr})
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(url(controller='login',
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(url(controller='login',
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(url('reset_password'))
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(url('login_home'))
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(url(controller='login', action='index'),
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
1 NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (1203 lines changed) Show them Hide them
General Comments 0
You need to be logged in to leave comments. Login now