##// 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 [bumpversion]
1 [bumpversion]
2 current_version = 4.0.1
2 current_version = 4.1.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:rhodecode/VERSION]
5 [bumpversion:file:rhodecode/VERSION]
@@ -27,6 +27,7 b' module.exports = function(grunt) {'
27 '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
27 '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
28 '<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
28 '<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
29 '<%= dirs.js.src %>/plugins/jquery.debounce.js',
29 '<%= dirs.js.src %>/plugins/jquery.debounce.js',
30 '<%= dirs.js.src %>/plugins/jquery.mark.js',
30 '<%= dirs.js.src %>/plugins/jquery.timeago.js',
31 '<%= dirs.js.src %>/plugins/jquery.timeago.js',
31 '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
32 '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
32
33
@@ -59,7 +60,7 b' module.exports = function(grunt) {'
59 '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js',
60 '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js',
60
61
61 // Rhodecode components
62 // Rhodecode components
62 '<%= dirs.js.src %>/rhodecode/pyroutes.js',
63 '<%= dirs.js.src %>/rhodecode/init.js',
63 '<%= dirs.js.src %>/rhodecode/codemirror.js',
64 '<%= dirs.js.src %>/rhodecode/codemirror.js',
64 '<%= dirs.js.src %>/rhodecode/comments.js',
65 '<%= dirs.js.src %>/rhodecode/comments.js',
65 '<%= dirs.js.src %>/rhodecode/constants.js',
66 '<%= dirs.js.src %>/rhodecode/constants.js',
@@ -34,9 +34,10 b' pdebug = false'
34 host = 127.0.0.1
34 host = 127.0.0.1
35 port = 5000
35 port = 5000
36
36
37 ##########################
37 ##################################
38 ## WAITRESS WSGI SERVER ##
38 ## WAITRESS WSGI SERVER ##
39 ##########################
39 ## Recommended for Development ##
40 ##################################
40 use = egg:waitress#main
41 use = egg:waitress#main
41 ## number of worker threads
42 ## number of worker threads
42 threads = 5
43 threads = 5
@@ -56,7 +57,7 b' asyncore_use_poll = true'
56 ## when this option is set to more than one worker, recommended
57 ## when this option is set to more than one worker, recommended
57 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
58 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
58 ## The `instance_id = *` must be set in the [app:main] section below
59 ## The `instance_id = *` must be set in the [app:main] section below
59 #workers = 1
60 #workers = 2
60 ## number of threads for each of the worker, must be set to 1 for gevent
61 ## number of threads for each of the worker, must be set to 1 for gevent
61 ## generally recommened to be at 1
62 ## generally recommened to be at 1
62 #threads = 1
63 #threads = 1
@@ -71,7 +72,7 b' asyncore_use_poll = true'
71 ## restarted, could prevent memory leaks
72 ## restarted, could prevent memory leaks
72 #max_requests = 1000
73 #max_requests = 1000
73 #max_requests_jitter = 30
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 ## gets killed and restarted. Set to 6hrs
76 ## gets killed and restarted. Set to 6hrs
76 #timeout = 21600
77 #timeout = 21600
77
78
@@ -199,6 +200,21 b' default_encoding = UTF-8'
199 ## all running rhodecode instances. Leave empty if you don't use it
200 ## all running rhodecode instances. Leave empty if you don't use it
200 instance_id =
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 ## alternative return HTTP header for failed authentication. Default HTTP
218 ## alternative return HTTP header for failed authentication. Default HTTP
203 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
219 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
204 ## handling that causing a series of failed authentication calls.
220 ## handling that causing a series of failed authentication calls.
@@ -356,12 +372,17 b' beaker.session.auto = false'
356 ###################################
372 ###################################
357 ## SEARCH INDEXING CONFIGURATION ##
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 search.module = rhodecode.lib.index.whoosh
380 search.module = rhodecode.lib.index.whoosh
361 search.location = %(here)s/data/index
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 ## Appenlight is tailored to work with RhodeCode, see
388 ## Appenlight is tailored to work with RhodeCode, see
@@ -372,7 +393,7 b' appenlight = false'
372
393
373 appenlight.server_url = https://api.appenlight.com
394 appenlight.server_url = https://api.appenlight.com
374 appenlight.api_key = YOUR_API_KEY
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 # used for JS client
398 # used for JS client
378 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
399 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
@@ -462,16 +483,26 b' sqlalchemy.db1.convert_unicode = true'
462 ##################
483 ##################
463 vcs.server.enable = true
484 vcs.server.enable = true
464 vcs.server = localhost:9900
485 vcs.server = localhost:9900
465 # Available protocols: pyro4, http
466 vcs.server.protocol = pyro4
467
486
468 # available impl:
487 ## Web server connectivity protocol, responsible for web based VCS operatations
469 # vcsserver.scm_app (EE only, for testing),
488 ## Available protocols are:
470 # rhodecode.lib.middleware.utils.scm_app_http
489 ## `pyro4` - using pyro4 server
471 # pyro4
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 #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http
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 vcs.server.log_level = debug
504 vcs.server.log_level = debug
505 ## Start VCSServer with this instance as a subprocess, usefull for development
475 vcs.start_server = true
506 vcs.start_server = true
476 vcs.backends = hg, git, svn
507 vcs.backends = hg, git, svn
477 vcs.connection_timeout = 3600
508 vcs.connection_timeout = 3600
@@ -34,46 +34,47 b' pdebug = false'
34 host = 127.0.0.1
34 host = 127.0.0.1
35 port = 5000
35 port = 5000
36
36
37 ##########################
37 ##################################
38 ## WAITRESS WSGI SERVER ##
38 ## WAITRESS WSGI SERVER ##
39 ##########################
39 ## Recommended for Development ##
40 use = egg:waitress#main
40 ##################################
41 #use = egg:waitress#main
41 ## number of worker threads
42 ## number of worker threads
42 threads = 5
43 #threads = 5
43 ## MAX BODY SIZE 100GB
44 ## MAX BODY SIZE 100GB
44 max_request_body_size = 107374182400
45 #max_request_body_size = 107374182400
45 ## Use poll instead of select, fixes file descriptors limits problems.
46 ## Use poll instead of select, fixes file descriptors limits problems.
46 ## May not work on old windows systems.
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 ## GUNICORN WSGI SERVER ##
52 ## GUNICORN WSGI SERVER ##
52 ##########################
53 ##########################
53 ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini>
54 ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini>
54 #use = egg:gunicorn#main
55 use = egg:gunicorn#main
55 ## Sets the number of process workers. You must set `instance_id = *`
56 ## Sets the number of process workers. You must set `instance_id = *`
56 ## when this option is set to more than one worker, recommended
57 ## when this option is set to more than one worker, recommended
57 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
58 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
58 ## The `instance_id = *` must be set in the [app:main] section below
59 ## The `instance_id = *` must be set in the [app:main] section below
59 #workers = 1
60 workers = 2
60 ## number of threads for each of the worker, must be set to 1 for gevent
61 ## number of threads for each of the worker, must be set to 1 for gevent
61 ## generally recommened to be at 1
62 ## generally recommened to be at 1
62 #threads = 1
63 #threads = 1
63 ## process name
64 ## process name
64 #proc_name = rhodecode
65 proc_name = rhodecode
65 ## type of worker class, one of sync, gevent
66 ## type of worker class, one of sync, gevent
66 ## recommended for bigger setup is using of of other than sync one
67 ## recommended for bigger setup is using of of other than sync one
67 #worker_class = sync
68 worker_class = sync
68 ## The maximum number of simultaneous clients. Valid only for Gevent
69 ## The maximum number of simultaneous clients. Valid only for Gevent
69 #worker_connections = 10
70 #worker_connections = 10
70 ## max number of requests that worker will handle before being gracefully
71 ## max number of requests that worker will handle before being gracefully
71 ## restarted, could prevent memory leaks
72 ## restarted, could prevent memory leaks
72 #max_requests = 1000
73 max_requests = 1000
73 #max_requests_jitter = 30
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 ## gets killed and restarted. Set to 6hrs
76 ## gets killed and restarted. Set to 6hrs
76 #timeout = 21600
77 timeout = 21600
77
78
78
79
79 ## prefix middleware for RhodeCode, disables force_https flag.
80 ## prefix middleware for RhodeCode, disables force_https flag.
@@ -173,6 +174,21 b' default_encoding = UTF-8'
173 ## all running rhodecode instances. Leave empty if you don't use it
174 ## all running rhodecode instances. Leave empty if you don't use it
174 instance_id =
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 ## alternative return HTTP header for failed authentication. Default HTTP
192 ## alternative return HTTP header for failed authentication. Default HTTP
177 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
193 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
178 ## handling that causing a series of failed authentication calls.
194 ## handling that causing a series of failed authentication calls.
@@ -304,7 +320,7 b' beaker.session.data_dir = %(here)s/data/'
304
320
305 beaker.session.key = rhodecode
321 beaker.session.key = rhodecode
306 beaker.session.secret = production-rc-uytcxaz
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 ## Secure encrypted cookie. Requires AES and AES python libraries
325 ## Secure encrypted cookie. Requires AES and AES python libraries
310 ## you must disable beaker.session.secret to use this
326 ## you must disable beaker.session.secret to use this
@@ -330,12 +346,17 b' beaker.session.auto = false'
330 ###################################
346 ###################################
331 ## SEARCH INDEXING CONFIGURATION ##
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 search.module = rhodecode.lib.index.whoosh
354 search.module = rhodecode.lib.index.whoosh
335 search.location = %(here)s/data/index
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 ## Appenlight is tailored to work with RhodeCode, see
362 ## Appenlight is tailored to work with RhodeCode, see
@@ -346,7 +367,7 b' appenlight = false'
346
367
347 appenlight.server_url = https://api.appenlight.com
368 appenlight.server_url = https://api.appenlight.com
348 appenlight.api_key = YOUR_API_KEY
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 # used for JS client
372 # used for JS client
352 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
373 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
@@ -401,11 +422,6 b' appenlight.log_namespace_blacklist ='
401 set debug = false
422 set debug = false
402
423
403
424
404 ##############
405 ## STYLING ##
406 ##############
407 debug_style = false
408
409 #########################################################
425 #########################################################
410 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
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 vcs.server.enable = true
453 vcs.server.enable = true
438 vcs.server = localhost:9900
454 vcs.server = localhost:9900
439 # Available protocols: pyro4, http
440 vcs.server.protocol = pyro4
441
455
442 # available impl:
456 ## Web server connectivity protocol, responsible for web based VCS operatations
443 # vcsserver.scm_app (EE only, for testing),
457 ## Available protocols are:
444 # rhodecode.lib.middleware.utils.scm_app_http
458 ## `pyro4` - using pyro4 server
445 # pyro4
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 #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http
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 vcs.server.log_level = info
473 vcs.server.log_level = info
474 ## Start VCSServer with this instance as a subprocess, usefull for development
449 vcs.start_server = false
475 vcs.start_server = false
450 vcs.backends = hg, git, svn
476 vcs.backends = hg, git, svn
451 vcs.connection_timeout = 3600
477 vcs.connection_timeout = 3600
@@ -85,7 +85,7 b' let'
85 pythonLocalOverrides = self: super: {
85 pythonLocalOverrides = self: super: {
86 rhodecode-enterprise-ce =
86 rhodecode-enterprise-ce =
87 let
87 let
88 version = "${builtins.readFile ./rhodecode/VERSION}";
88 version = builtins.readFile ./rhodecode/VERSION;
89 linkNodeModules = ''
89 linkNodeModules = ''
90 echo "Link node packages"
90 echo "Link node packages"
91 # TODO: check if this adds stuff as a dependency, closure size
91 # TODO: check if this adds stuff as a dependency, closure size
@@ -119,7 +119,9 b' let'
119 # TODO: johbo: Make a nicer way to expose the parts. Maybe
119 # TODO: johbo: Make a nicer way to expose the parts. Maybe
120 # pkgs/default.nix?
120 # pkgs/default.nix?
121 passthru = {
121 passthru = {
122 inherit myPythonPackagesUnfix;
122 inherit
123 pythonLocalOverrides
124 myPythonPackagesUnfix;
123 pythonPackages = self;
125 pythonPackages = self;
124 };
126 };
125
127
@@ -160,6 +162,7 b' let'
160 ln -s ${self.supervisor}/bin/supervisor* $out/bin/
162 ln -s ${self.supervisor}/bin/supervisor* $out/bin/
161 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
163 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
162 ln -s ${self.PasteScript}/bin/paster $out/bin/
164 ln -s ${self.PasteScript}/bin/paster $out/bin/
165 ln -s ${self.pyramid}/bin/* $out/bin/ #*/
163
166
164 # rhodecode-tools
167 # rhodecode-tools
165 # TODO: johbo: re-think this. Do the tools import anything from enterprise?
168 # TODO: johbo: re-think this. Do the tools import anything from enterprise?
@@ -169,6 +172,7 b' let'
169 for file in $out/bin/*; do #*/
172 for file in $out/bin/*; do #*/
170 wrapProgram $file \
173 wrapProgram $file \
171 --prefix PYTHONPATH : $PYTHONPATH \
174 --prefix PYTHONPATH : $PYTHONPATH \
175 --prefix PATH : $PATH \
172 --set PYTHONHASHSEED random
176 --set PYTHONHASHSEED random
173 done
177 done
174
178
@@ -9,24 +9,24 b' Here is a sample configuration file for '
9 ServerName hg.myserver.com
9 ServerName hg.myserver.com
10 ServerAlias hg.myserver.com
10 ServerAlias hg.myserver.com
11
11
12 ## uncomment root directive if you want to serve static files by nginx
12 ## uncomment root directive if you want to serve static files by
13 ## requires static_files = false in .ini file
13 ## Apache requires static_files = false in .ini file
14 DocumentRoot /path/to/installation/rhodecode/public
14 #DocumentRoot /path/to/rhodecode/installation/public
15
15
16 <Proxy *>
16 <Proxy *>
17 Order allow,deny
17 Order allow,deny
18 Allow from all
18 Allow from all
19 </Proxy>
19 </Proxy>
20
20
21 #important !
21 ## Important !
22 #Directive to properly generate url (clone url) for pylons
22 ## Directive to properly generate url (clone url) for pylons
23 ProxyPreserveHost On
23 ProxyPreserveHost On
24
24
25 #rhodecode instance
25 ## RhodeCode instance running
26 ProxyPass / http://127.0.0.1:5000/
26 ProxyPass / http://127.0.0.1:10002/
27 ProxyPassReverse / http://127.0.0.1:5000/
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 #SetEnvIf X-Url-Scheme https HTTPS=1
30 #SetEnvIf X-Url-Scheme https HTTPS=1
31
31
32 </VirtualHost>
32 </VirtualHost>
@@ -3,7 +3,15 b''
3 Full-text Search
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 To run the indexer you need to use an |authtoken| with admin rights to all
15 To run the indexer you need to use an |authtoken| with admin rights to all
8 |repos|.
16 |repos|.
9
17
@@ -232,4 +240,33 b' use the following example :file:`mapping'
232 max_filesize = 800MB
240 max_filesize = 800MB
233 commit_parse_limit = 20000
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 .. _Whoosh: https://pypi.python.org/pypi/Whoosh/
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 upstream rc {
8 upstream rc {
9
9
10 server 127.0.0.1:5000;
10 server 127.0.0.1:10002;
11
11
12 # add more instances for load balancing
12 # add more instances for load balancing
13 # server 127.0.0.1:5001;
13 # server 127.0.0.1:10003;
14 # server 127.0.0.1:5002;
14 # server 127.0.0.1:10004;
15 }
15 }
16
16
17 ## gist alias
17 ## gist alias
@@ -58,9 +58,10 b' Use the following example to configure N'
58
58
59 ## uncomment root directive if you want to serve static files by nginx
59 ## uncomment root directive if you want to serve static files by nginx
60 ## requires static_files = false in .ini file
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 include /etc/nginx/proxy.conf;
63 include /etc/nginx/proxy.conf;
64
64 location / {
65 location / {
65 try_files $uri @rhode;
66 try_files $uri @rhode;
66 }
67 }
@@ -64,6 +64,14 b' performance is more important than CPU p'
64 environment handling 1000s of users and |repos| you should deploy on a 12+
64 environment handling 1000s of users and |repos| you should deploy on a 12+
65 core 64GB RAM server. In short, the more RAM the better.
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 .. _config-rce-files:
75 .. _config-rce-files:
68
76
69 Configuration Files
77 Configuration Files
@@ -23,6 +23,8 b" rst_epilog = '''"
23 .. |RCV| replace:: RhodeCode Enterprise
23 .. |RCV| replace:: RhodeCode Enterprise
24 .. |RCM| replace:: RhodeCode Enterprise
24 .. |RCM| replace:: RhodeCode Enterprise
25 .. |RCE| replace:: RhodeCode Enterprise
25 .. |RCE| replace:: RhodeCode Enterprise
26 .. |RCCE| replace:: RhodeCode Community
27 .. |RCEE| replace:: RhodeCode Enterprise
26 .. |RCX| replace:: RhodeCode Extensions
28 .. |RCX| replace:: RhodeCode Extensions
27 .. |RCT| replace:: RhodeCode Tools
29 .. |RCT| replace:: RhodeCode Tools
28 .. |RCEBOLD| replace:: **RhodeCode Enterprise**
30 .. |RCEBOLD| replace:: **RhodeCode Enterprise**
@@ -5,12 +5,12 b' Make Database Changes'
5
5
6 .. important::
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 upgrade the database, and also remap and rescan the |repos|. More detailed
9 upgrade the database, and also remap and rescan the |repos|. More detailed
10 information is available in the
10 information is available in the
11 :ref:`Alternative upgrade documentation <control:install-port>`.
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 use the following steps:
14 use the following steps:
15
15
16 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The
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 credentials during |RCE| installation. See the relevant database
17 credentials during |RCE| installation. See the relevant database
18 documentation for more details.
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 1. Download the latest |RCC| installer from your `rhodecode.com`_ profile
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 2. Run the |RCC| installer and accept the End User Licence using the
26 2. Run the |RCC| installer and accept the End User Licence using the
25 following example:
27 following example:
26
28
@@ -45,13 +47,18 b' 3. Install a VCS Server, and configure i'
45 Added process group vcsserver-1
47 Added process group vcsserver-1
46
48
47
49
48 4. Install |RCE|. If using MySQL or PostgreSQL, during installation you'll be
50 4. Install |RCEE| or |RCCE|. If using MySQL or PostgreSQL, during
49 asked for your database credentials, so have them at hand. You don't need
51 installation you'll be asked for your database credentials, so have them at hand.
50 any for SQLite.
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 .. code-block:: bash
55 .. code-block:: bash
53 :emphasize-lines: 11-16
56 :emphasize-lines: 11-16
54
57
58 $ rccontrol install Community
59
60 or
61
55 $ rccontrol install Enterprise
62 $ rccontrol install Enterprise
56
63
57 Username [admin]: username
64 Username [admin]: username
@@ -69,8 +76,8 b' 4. Install |RCE|. If using MySQL or Post'
69 Database password: somepassword
76 Database password: somepassword
70 Database name: example-db-name
77 Database name: example-db-name
71
78
72 5. Check the status of your installation. You |RCE| instance runs on the URL
79 5. Check the status of your installation. You |RCEE|/|RCCE| instance runs
73 displayed in the status message.
80 on the URL displayed in the status message.
74
81
75 .. code-block:: bash
82 .. code-block:: bash
76
83
@@ -79,13 +86,13 b' 5. Check the status of your installation'
79 - NAME: enterprise-1
86 - NAME: enterprise-1
80 - STATUS: RUNNING
87 - STATUS: RUNNING
81 - TYPE: Enterprise
88 - TYPE: Enterprise
82 - VERSION: 3.3.0
89 - VERSION: 4.1.0
83 - URL: http://127.0.0.1:10003
90 - URL: http://127.0.0.1:10003
84
91
85 - NAME: vcsserver-1
92 - NAME: vcsserver-1
86 - STATUS: RUNNING
93 - STATUS: RUNNING
87 - TYPE: VCSServer
94 - TYPE: VCSServer
88 - VERSION: 3.3.0
95 - VERSION: 4.1.0
89 - URL: http://127.0.0.1:10001
96 - URL: http://127.0.0.1:10001
90
97
91 .. note::
98 .. note::
@@ -37,6 +37,10 b' New Features'
37 Github, Twitter, Bitbucket and Google. It's possible now to use your
37 Github, Twitter, Bitbucket and Google. It's possible now to use your
38 Google account to log in to RhodeCode and take advantage of things like 2FA.
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 Security
44 Security
41 ^^^^^^^^
45 ^^^^^^^^
42
46
@@ -46,8 +50,10 b' Security'
46 Performance
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 - Improved file tree loading speed
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 Fixes
59 Fixes
@@ -6,6 +6,10 b' Release Notes'
6 |RCE| 4.x Versions
6 |RCE| 4.x Versions
7 ------------------
7 ------------------
8
8
9 .. toctree::
10 :maxdepth: 1
11
12 release-notes-4.1.0.rst
9 release-notes-4.0.1.rst
13 release-notes-4.0.1.rst
10 release-notes-4.0.0.rst
14 release-notes-4.0.0.rst
11
15
@@ -1,13 +1,12 b''
1 diff --git a/requirements.txt b/requirements.txt
1 diff --git a/requirements.txt b/requirements.txt
2 --- a/requirements.txt
2 --- a/requirements.txt
3 +++ b/requirements.txt
3 +++ b/requirements.txt
4 @@ -1,8 +1,8 @@
4 @@ -3,7 +3,7 @@future==0.14.3
5 click==5.1
6 future==0.14.3
7 six==1.9.0
5 six==1.9.0
8 mako==1.0.1
6 mako==1.0.1
9 markupsafe==0.23
7 markupsafe==0.23
10 -requests==2.5.1
8 -requests==2.5.1
11 +requests
9 +requests
10 #responses
12 whoosh==2.7.0
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 kombu = super.kombu.override (attrs: {
38 kombu = super.kombu.override (attrs: {
25 # The current version of kombu needs some patching to work with the
39 # The current version of kombu needs some patching to work with the
26 # other libs. Should be removed once we update celery and kombu.
40 # other libs. Should be removed once we update celery and kombu.
@@ -359,16 +359,6 b''
359 md5 = "898bc87e54f278055b561316ba73e222";
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 click = super.buildPythonPackage {
362 click = super.buildPythonPackage {
373 name = "click-5.1";
363 name = "click-5.1";
374 buildInputs = with self; [];
364 buildInputs = with self; [];
@@ -490,13 +480,23 b''
490 };
480 };
491 };
481 };
492 elasticsearch = super.buildPythonPackage {
482 elasticsearch = super.buildPythonPackage {
493 name = "elasticsearch-1.9.0";
483 name = "elasticsearch-2.3.0";
494 buildInputs = with self; [];
484 buildInputs = with self; [];
495 doCheck = false;
485 doCheck = false;
496 propagatedBuildInputs = with self; [urllib3];
486 propagatedBuildInputs = with self; [urllib3];
497 src = fetchurl {
487 src = fetchurl {
498 url = "https://pypi.python.org/packages/13/9b/540e311b31a10c2a904acfb08030c656047e5c7ba479d35df2799e5dccfe/elasticsearch-1.9.0.tar.gz";
488 url = "https://pypi.python.org/packages/10/35/5fd52c5f0b0ee405ed4b5195e8bce44c5e041787680dc7b94b8071cac600/elasticsearch-2.3.0.tar.gz";
499 md5 = "3550390baea1639479f79758d66ab032";
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 flake8 = super.buildPythonPackage {
502 flake8 = super.buildPythonPackage {
@@ -540,7 +540,7 b''
540 };
540 };
541 };
541 };
542 gprof2dot = super.buildPythonPackage {
542 gprof2dot = super.buildPythonPackage {
543 name = "gprof2dot-2015.12.1";
543 name = "gprof2dot-2015.12.01";
544 buildInputs = with self; [];
544 buildInputs = with self; [];
545 doCheck = false;
545 doCheck = false;
546 propagatedBuildInputs = with self; [];
546 propagatedBuildInputs = with self; [];
@@ -550,13 +550,13 b''
550 };
550 };
551 };
551 };
552 greenlet = super.buildPythonPackage {
552 greenlet = super.buildPythonPackage {
553 name = "greenlet-0.4.7";
553 name = "greenlet-0.4.9";
554 buildInputs = with self; [];
554 buildInputs = with self; [];
555 doCheck = false;
555 doCheck = false;
556 propagatedBuildInputs = with self; [];
556 propagatedBuildInputs = with self; [];
557 src = fetchurl {
557 src = fetchurl {
558 url = "https://pypi.python.org/packages/7a/9f/a1a0d9bdf3203ae1502c5a8434fe89d323599d78a106985bc327351a69d4/greenlet-0.4.7.zip";
558 url = "https://pypi.python.org/packages/4e/3d/9d421539b74e33608b245092870156b2e171fb49f2b51390aa4641eecb4a/greenlet-0.4.9.zip";
559 md5 = "c2333a8ff30fa75c5d5ec0e67b461086";
559 md5 = "c6659cdb2a5e591723e629d2eef22e82";
560 };
560 };
561 };
561 };
562 gunicorn = super.buildPythonPackage {
562 gunicorn = super.buildPythonPackage {
@@ -603,7 +603,7 b''
603 name = "ipython-3.1.0";
603 name = "ipython-3.1.0";
604 buildInputs = with self; [];
604 buildInputs = with self; [];
605 doCheck = false;
605 doCheck = false;
606 propagatedBuildInputs = with self; [gnureadline];
606 propagatedBuildInputs = with self; [];
607 src = fetchurl {
607 src = fetchurl {
608 url = "https://pypi.python.org/packages/06/91/120c0835254c120af89f066afaabf81289bc2726c1fc3ca0555df6882f58/ipython-3.1.0.tar.gz";
608 url = "https://pypi.python.org/packages/06/91/120c0835254c120af89f066afaabf81289bc2726c1fc3ca0555df6882f58/ipython-3.1.0.tar.gz";
609 md5 = "a749d90c16068687b0ec45a27e72ef8f";
609 md5 = "a749d90c16068687b0ec45a27e72ef8f";
@@ -799,16 +799,6 b''
799 md5 = "47b4eac84118e2606658122104e62072";
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 pyflakes = super.buildPythonPackage {
802 pyflakes = super.buildPythonPackage {
813 name = "pyflakes-0.8.1";
803 name = "pyflakes-0.8.1";
814 buildInputs = with self; [];
804 buildInputs = with self; [];
@@ -1050,20 +1040,20 b''
1050 };
1040 };
1051 };
1041 };
1052 rhodecode-enterprise-ce = super.buildPythonPackage {
1042 rhodecode-enterprise-ce = super.buildPythonPackage {
1053 name = "rhodecode-enterprise-ce-4.0.1";
1043 name = "rhodecode-enterprise-ce-4.1.0";
1054 buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner];
1044 buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner];
1055 doCheck = true;
1045 doCheck = true;
1056 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];
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 src = ./.;
1047 src = ./.;
1058 };
1048 };
1059 rhodecode-tools = super.buildPythonPackage {
1049 rhodecode-tools = super.buildPythonPackage {
1060 name = "rhodecode-tools-0.7.1";
1050 name = "rhodecode-tools-0.8.3";
1061 buildInputs = with self; [];
1051 buildInputs = with self; [];
1062 doCheck = false;
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 src = fetchurl {
1054 src = fetchurl {
1065 url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.7.1.zip";
1055 url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip";
1066 md5 = "91daea803aaa264ce7a8213bc2220d4c";
1056 md5 = "9acdfd71b8ddf4056057065f37ab9ccb";
1067 };
1057 };
1068 };
1058 };
1069 serpent = super.buildPythonPackage {
1059 serpent = super.buildPythonPackage {
@@ -11,7 +11,6 b' MySQL-python==1.2.5'
11 Paste==2.0.2
11 Paste==2.0.2
12 PasteDeploy==1.5.2
12 PasteDeploy==1.5.2
13 PasteScript==1.7.5
13 PasteScript==1.7.5
14 pyelasticsearch==1.4
15 Pygments==2.0.2
14 Pygments==2.0.2
16
15
17 # TODO: This version is not available on PyPI
16 # TODO: This version is not available on PyPI
@@ -70,13 +69,14 b' flake8==2.4.1'
70 future==0.14.3
69 future==0.14.3
71 futures==3.0.2
70 futures==3.0.2
72 gprof2dot==2015.12.1
71 gprof2dot==2015.12.1
73 greenlet==0.4.7
72 greenlet==0.4.9
74 gunicorn==19.6.0
73 gunicorn==19.6.0
75
74
76 # TODO: Needs subvertpy and blows up without Subversion headers,
75 # TODO: Needs subvertpy and blows up without Subversion headers,
77 # actually we should not need this for Enterprise at all.
76 # actually we should not need this for Enterprise at all.
78 # hgsubversion==1.8.2
77 # hgsubversion==1.8.2
79
78
79 gnureadline==6.3.3
80 infrae.cache==1.0.1
80 infrae.cache==1.0.1
81 invoke==0.11.1
81 invoke==0.11.1
82 ipdb==0.8
82 ipdb==0.8
@@ -124,7 +124,7 b' pyzmq==14.6.0'
124 # TODO: This is not available in public
124 # TODO: This is not available in public
125 # rc-testdata==0.2.0
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 recaptcha-client==1.0.6
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 EXTENSIONS = {}
47 EXTENSIONS = {}
48
48
49 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
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 __platform__ = platform.system()
51 __platform__ = platform.system()
52 __license__ = 'AGPLv3, and Commercial License'
52 __license__ = 'AGPLv3, and Commercial License'
53 __author__ = 'RhodeCode GmbH'
53 __author__ = 'RhodeCode GmbH'
@@ -24,67 +24,81 b' import pytest'
24 from rhodecode.model.repo import RepoModel
24 from rhodecode.model.repo import RepoModel
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
26 from rhodecode.api.tests.utils import (
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 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29
29
30
30
31 fixture = Fixture()
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 @pytest.mark.usefixtures("testuser_api", "app")
37 @pytest.mark.usefixtures("testuser_api", "app")
35 class TestApiUpdateRepo(object):
38 class TestApiUpdateRepo(object):
36 @pytest.mark.parametrize("changing_attr, updates", [
39
37 ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
40 @pytest.mark.parametrize("updates, expected", [
38 ('description', {'description': 'new description'}),
41 ({'owner': TEST_USER_REGULAR_LOGIN}, SAME_AS_UPDATES),
39 ('active', {'active': True}),
42 ({'description': 'new description'}, SAME_AS_UPDATES),
40 ('active', {'active': False}),
43 ({'clone_uri': 'http://foo.com/repo'}, SAME_AS_UPDATES),
41 ('clone_uri', {'clone_uri': 'http://foo.com/repo'}),
44 ({'clone_uri': None}, {'clone_uri': ''}),
42 ('clone_uri', {'clone_uri': None}),
45 ({'clone_uri': ''}, {'clone_uri': ''}),
43 ('landing_rev', {'landing_rev': 'branch:master'}),
46 ({'landing_rev': 'branch:master'}, {'landing_rev': ['branch','master']}),
44 ('enable_statistics', {'enable_statistics': True}),
47 ({'enable_statistics': True}, SAME_AS_UPDATES),
45 ('enable_locking', {'enable_locking': True}),
48 ({'enable_locking': True}, SAME_AS_UPDATES),
46 ('enable_downloads', {'enable_downloads': True}),
49 ({'enable_downloads': True}, SAME_AS_UPDATES),
47 ('name', {'name': 'new_repo_name'}),
50 ({'name': 'new_repo_name'}, {'repo_name': 'new_repo_name'}),
48 ('repo_group', {'group': 'test_group_for_update'}),
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):
54 def test_api_update_repo(self, updates, expected, backend):
51 repo_name = 'api_update_me'
55 repo_name = UPDATE_REPO_NAME
52 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
56 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
53 if changing_attr == 'repo_group':
57 if updates.get('group'):
54 fixture.create_repo_group(updates['group'])
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 id_, params = build_data(
67 id_, params = build_data(
57 self.apikey, 'update_repo', repoid=repo_name, **updates)
68 self.apikey, 'update_repo', repoid=repo_name, **updates)
58 response = api_call(self.app, params)
69 response = api_call(self.app, params)
59 if changing_attr == 'name':
70
71 if updates.get('name'):
60 repo_name = updates['name']
72 repo_name = updates['name']
61 if changing_attr == 'repo_group':
73 if updates.get('group'):
62 repo_name = '/'.join([updates['group'], repo_name])
74 repo_name = '/'.join([updates['group'], repo_name])
75
63 try:
76 try:
64 expected = {
77 expected = {
65 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
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 assert_ok(id_, expected, given=response.body)
81 assert_ok(id_, expected, given=response.body)
69 finally:
82 finally:
70 fixture.destroy_repo(repo_name)
83 fixture.destroy_repo(repo_name)
71 if changing_attr == 'repo_group':
84 if updates.get('group'):
72
73 fixture.destroy_repo_group(updates['group'])
85 fixture.destroy_repo_group(updates['group'])
74
86
75 def test_api_update_repo_fork_of_field(self, backend):
87 def test_api_update_repo_fork_of_field(self, backend):
76 master_repo = backend.create_repo()
88 master_repo = backend.create_repo()
77 repo = backend.create_repo()
89 repo = backend.create_repo()
78
79 updates = {
90 updates = {
80 'fork_of': master_repo.repo_name
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 id_, params = build_data(
96 id_, params = build_data(
83 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
97 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
84 response = api_call(self.app, params)
98 response = api_call(self.app, params)
85 expected = {
99 expected = {
86 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
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 assert_ok(id_, expected, given=response.body)
103 assert_ok(id_, expected, given=response.body)
90 result = response.json['result']['repository']
104 result = response.json['result']['repository']
@@ -131,7 +145,7 b' class TestApiUpdateRepo(object):'
131
145
132 @mock.patch.object(RepoModel, 'update', crash)
146 @mock.patch.object(RepoModel, 'update', crash)
133 def test_api_update_repo_exception_occurred(self, backend):
147 def test_api_update_repo_exception_occurred(self, backend):
134 repo_name = 'api_update_me'
148 repo_name = UPDATE_REPO_NAME
135 fixture.create_repo(repo_name, repo_type=backend.alias)
149 fixture.create_repo(repo_name, repo_type=backend.alias)
136 id_, params = build_data(
150 id_, params = build_data(
137 self.apikey, 'update_repo', repoid=repo_name,
151 self.apikey, 'update_repo', repoid=repo_name,
@@ -25,7 +25,7 b' from rhodecode.model.user import UserMod'
25 from rhodecode.model.user_group import UserGroupModel
25 from rhodecode.model.user_group import UserGroupModel
26 from rhodecode.tests import TEST_USER_REGULAR_LOGIN
26 from rhodecode.tests import TEST_USER_REGULAR_LOGIN
27 from rhodecode.api.tests.utils import (
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 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
@@ -40,14 +40,18 b' class TestUpdateUserGroup(object):'
40 def test_api_update_user_group(self, changing_attr, updates, user_util):
40 def test_api_update_user_group(self, changing_attr, updates, user_util):
41 user_group = user_util.create_user_group()
41 user_group = user_util.create_user_group()
42 group_name = user_group.users_group_name
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 id_, params = build_data(
46 id_, params = build_data(
44 self.apikey, 'update_user_group', usergroupid=group_name,
47 self.apikey, 'update_user_group', usergroupid=group_name,
45 **updates)
48 **updates)
46 response = api_call(self.app, params)
49 response = api_call(self.app, params)
50
47 expected = {
51 expected = {
48 'msg': 'updated user group ID:%s %s' % (
52 'msg': 'updated user group ID:%s %s' % (
49 user_group.users_group_id, user_group.users_group_name),
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 assert_ok(id_, expected, given=response.body)
56 assert_ok(id_, expected, given=response.body)
53
57
@@ -63,6 +67,10 b' class TestUpdateUserGroup(object):'
63 self, changing_attr, updates, user_util):
67 self, changing_attr, updates, user_util):
64 user_group = user_util.create_user_group()
68 user_group = user_util.create_user_group()
65 group_name = user_group.users_group_name
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 # grant permission to this user
74 # grant permission to this user
67 user = UserModel().get_by_username(self.TEST_USER_LOGIN)
75 user = UserModel().get_by_username(self.TEST_USER_LOGIN)
68
76
@@ -75,7 +83,7 b' class TestUpdateUserGroup(object):'
75 expected = {
83 expected = {
76 'msg': 'updated user group ID:%s %s' % (
84 'msg': 'updated user group ID:%s %s' % (
77 user_group.users_group_id, user_group.users_group_name),
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 assert_ok(id_, expected, given=response.body)
88 assert_ok(id_, expected, given=response.body)
81
89
@@ -323,7 +323,7 b' def get_repo_changeset(request, apiuser,'
323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
324 details=Optional('basic')):
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 from the `start_rev` option.
327 from the `start_rev` option.
328
328
329 Additional parameters define the amount of details returned by this
329 Additional parameters define the amount of details returned by this
@@ -338,7 +338,7 b' def get_repo_changesets(request, apiuser'
338 :type repoid: str or int
338 :type repoid: str or int
339 :param start_rev: The starting revision from where to get changesets.
339 :param start_rev: The starting revision from where to get changesets.
340 :type start_rev: str
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 :type limit: str or int
342 :type limit: str or int
343 :param details: Set the level of detail returned. Valid option are:
343 :param details: Set the level of detail returned. Valid option are:
344 ``basic``, ``extended`` and ``full``.
344 ``basic``, ``extended`` and ``full``.
@@ -370,14 +370,17 b' def get_repo_changesets(request, apiuser'
370
370
371 vcs_repo = repo.scm_instance()
371 vcs_repo = repo.scm_instance()
372 # SVN needs a special case to distinguish its index and commit id
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 start_rev = vcs_repo.commit_ids[0]
374 start_rev = vcs_repo.commit_ids[0]
375
375
376 try:
376 try:
377 commits = repo.scm_instance().get_commits(
377 commits = vcs_repo.get_commits(
378 start_id=start_rev, pre_load=pre_load)
378 start_id=start_rev, pre_load=pre_load)
379 except TypeError as e:
379 except TypeError as e:
380 raise JSONRPCError(e.message)
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 ret = []
385 ret = []
383 for cnt, commit in enumerate(commits):
386 for cnt, commit in enumerate(commits):
@@ -19,6 +19,7 b''
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import importlib
22
23
23 from pkg_resources import iter_entry_points
24 from pkg_resources import iter_entry_points
24 from pyramid.authentication import SessionAuthenticationPolicy
25 from pyramid.authentication import SessionAuthenticationPolicy
@@ -27,9 +28,15 b' from rhodecode.authentication.registry i'
27 from rhodecode.authentication.routes import root_factory
28 from rhodecode.authentication.routes import root_factory
28 from rhodecode.authentication.routes import AuthnRootResource
29 from rhodecode.authentication.routes import AuthnRootResource
29 from rhodecode.config.routing import ADMIN_PREFIX
30 from rhodecode.config.routing import ADMIN_PREFIX
31 from rhodecode.model.settings import SettingsModel
32
30
33
31 log = logging.getLogger(__name__)
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 # TODO: Currently this is only used to discover the authentication plugins.
41 # TODO: Currently this is only used to discover the authentication plugins.
35 # Later on this may be used in a generic way to look up and include all kinds
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 # TODO: When refactoring this think about splitting it up into distinct
45 # TODO: When refactoring this think about splitting it up into distinct
39 # discover, load and include phases.
46 # discover, load and include phases.
40 def _discover_plugins(config, entry_point='enterprise.plugins1'):
47 def _discover_plugins(config, entry_point='enterprise.plugins1'):
41 _discovered_plugins = {}
42
43 for ep in iter_entry_points(entry_point):
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 log.debug('Plugin discovered: "%s"', plugin_id)
51 log.debug('Plugin discovered: "%s"', plugin_id)
52 try:
46 module = ep.load()
53 module = ep.load()
47 plugin = module(plugin_id=plugin_id)
54 plugin = module(plugin_id=plugin_id)
48 config.include(plugin.includeme)
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))
49
60
50 return _discovered_plugins
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
67
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 def includeme(config):
89 def includeme(config):
@@ -56,7 +92,7 b' def includeme(config):'
56 config.set_authentication_policy(authn_policy)
92 config.set_authentication_policy(authn_policy)
57
93
58 # Create authentication plugin registry and add it to the pyramid registry.
94 # Create authentication plugin registry and add it to the pyramid registry.
59 authn_registry = AuthenticationPluginRegistry()
95 authn_registry = AuthenticationPluginRegistry(config.get_settings())
60 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
96 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
61 config.registry.registerUtility(authn_registry)
97 config.registry.registerUtility(authn_registry)
62
98
@@ -83,3 +119,4 b' def includeme(config):'
83
119
84 # Auto discover authentication plugins and include their configuration.
120 # Auto discover authentication plugins and include their configuration.
85 _discover_plugins(config)
121 _discover_plugins(config)
122 _discover_legacy_plugins(config)
@@ -25,24 +25,18 b' Authentication modules'
25 import logging
25 import logging
26 import time
26 import time
27 import traceback
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 from pyramid.threadlocal import get_current_registry
30 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.ext.hybrid import hybrid_property
31 from sqlalchemy.ext.hybrid import hybrid_property
37
32
38 import rhodecode.lib.helpers as h
39 from rhodecode.authentication.interface import IAuthnPluginRegistry
33 from rhodecode.authentication.interface import IAuthnPluginRegistry
40 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
41 from rhodecode.lib import caches
35 from rhodecode.lib import caches
42 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
36 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
43 from rhodecode.lib.utils2 import md5_safe, safe_int
37 from rhodecode.lib.utils2 import md5_safe, safe_int
44 from rhodecode.lib.utils2 import safe_str
38 from rhodecode.lib.utils2 import safe_str
45 from rhodecode.model.db import User, ExternalIdentity
39 from rhodecode.model.db import User
46 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
47 from rhodecode.model.settings import SettingsModel
41 from rhodecode.model.settings import SettingsModel
48 from rhodecode.model.user import UserModel
42 from rhodecode.model.user import UserModel
@@ -226,17 +220,23 b' class RhodeCodeAuthPluginBase(object):'
226 """
220 """
227 raise NotImplementedError("Not implemented in base class")
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 @hybrid_property
231 @hybrid_property
230 def is_container_auth(self):
232 def is_container_auth(self):
231 """
233 """
232 Returns bool if this module uses container auth.
234 Deprecated method that indicates if this authentication plugin uses
233
235 HTTP headers as authentication method.
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
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 @hybrid_property
241 @hybrid_property
242 def allows_creating_users(self):
242 def allows_creating_users(self):
@@ -299,7 +299,7 b' class RhodeCodeAuthPluginBase(object):'
299 """
299 """
300 Helper method for user fetching in plugins, by default it's using
300 Helper method for user fetching in plugins, by default it's using
301 simple fetch by username, but this method can be custimized in plugins
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 :param username: username if given to fetch from database
304 :param username: username if given to fetch from database
305 :param kwargs: extra arguments needed for user fetching.
305 :param kwargs: extra arguments needed for user fetching.
@@ -477,131 +477,11 b' class RhodeCodeExternalAuthPlugin(RhodeC'
477 return auth
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 def loadplugin(plugin_id):
480 def loadplugin(plugin_id):
596 """
481 """
597 Loads and returns an instantiated authentication plugin.
482 Loads and returns an instantiated authentication plugin.
598 Returns the RhodeCodeAuthPluginBase subclass on success,
483 Returns the RhodeCodeAuthPluginBase subclass on success,
599 raises exceptions on failure.
484 or None 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
605 """
485 """
606 # TODO: Disusing pyramids thread locals to retrieve the registry.
486 # TODO: Disusing pyramids thread locals to retrieve the registry.
607 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
487 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
@@ -622,9 +502,9 b' def authenticate(username, password, env'
622 Authentication function used for access control,
502 Authentication function used for access control,
623 It tries to authenticate based on enabled authentication modules.
503 It tries to authenticate based on enabled authentication modules.
624
504
625 :param username: username can be empty for container auth
505 :param username: username can be empty for headers auth
626 :param password: password can be empty for container auth
506 :param password: password can be empty for headers auth
627 :param environ: environ headers passed for container auth
507 :param environ: environ headers passed for headers auth
628 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
508 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
629 :param skip_missing: ignores plugins that are in db but not in environment
509 :param skip_missing: ignores plugins that are in db but not in environment
630 :returns: None if auth failed, plugin_user dict if auth is correct
510 :returns: None if auth failed, plugin_user dict if auth is correct
@@ -632,51 +512,41 b' def authenticate(username, password, env'
632 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
512 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
633 raise ValueError('auth type must be on of http, vcs got "%s" instead'
513 raise ValueError('auth type must be on of http, vcs got "%s" instead'
634 % auth_type)
514 % auth_type)
635 container_only = environ and not (username and password)
515 headers_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)
639
516
640 if plugin is None:
517 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
641 log.warning('Authentication plugin missing: "{}"'.format(
518 for plugin in authn_registry.get_plugins_for_authentication():
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
650 plugin.set_auth_type(auth_type)
519 plugin.set_auth_type(auth_type)
651 user = plugin.get_user(username)
520 user = plugin.get_user(username)
652 display_user = user.username if user else username
521 display_user = user.username if user else username
653
522
654 if container_only and not plugin.is_container_auth:
523 if headers_only and not plugin.is_headers_auth:
655 log.debug('Auth type is for container only and plugin `%s` is not '
524 log.debug('Auth type is for headers only and plugin `%s` is not '
656 'container plugin, skipping...', plugin_id)
525 'headers plugin, skipping...', plugin.get_id())
657 continue
526 continue
658
527
659 # load plugin settings from RhodeCode database
528 # load plugin settings from RhodeCode database
660 plugin_settings = plugin.get_settings()
529 plugin_settings = plugin.get_settings()
661 log.debug('Plugin settings:%s', plugin_settings)
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 # use plugin's method of user extraction.
533 # use plugin's method of user extraction.
665 user = plugin.get_user(username, environ=environ,
534 user = plugin.get_user(username, environ=environ,
666 settings=plugin_settings)
535 settings=plugin_settings)
667 display_user = user.username if user else username
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 if not plugin.allows_authentication_from(user):
540 if not plugin.allows_authentication_from(user):
671 log.debug('Plugin %s does not accept user `%s` for authentication',
541 log.debug('Plugin %s does not accept user `%s` for authentication',
672 plugin_id, display_user)
542 plugin.get_id(), display_user)
673 continue
543 continue
674 else:
544 else:
675 log.debug('Plugin %s accepted user `%s` for authentication',
545 log.debug('Plugin %s accepted user `%s` for authentication',
676 plugin_id, display_user)
546 plugin.get_id(), display_user)
677
547
678 log.info('Authenticating user `%s` using %s plugin',
548 log.info('Authenticating user `%s` using %s plugin',
679 display_user, plugin_id)
549 display_user, plugin.get_id())
680
550
681 _cache_ttl = 0
551 _cache_ttl = 0
682
552
@@ -691,7 +561,7 b' def authenticate(username, password, env'
691 # get instance of cache manager configured for a namespace
561 # get instance of cache manager configured for a namespace
692 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
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 plugin_cache_active)
565 plugin_cache_active)
696
566
697 # for environ based password can be empty, but then the validation is
567 # for environ based password can be empty, but then the validation is
@@ -706,7 +576,7 b' def authenticate(username, password, env'
706 # then auth is correct.
576 # then auth is correct.
707 start = time.time()
577 start = time.time()
708 log.debug('Running plugin `%s` _authenticate method',
578 log.debug('Running plugin `%s` _authenticate method',
709 plugin_id)
579 plugin.get_id())
710
580
711 def auth_func():
581 def auth_func():
712 """
582 """
@@ -726,7 +596,7 b' def authenticate(username, password, env'
726 auth_time = time.time() - start
596 auth_time = time.time() - start
727 log.debug('Authentication for plugin `%s` completed in %.3fs, '
597 log.debug('Authentication for plugin `%s` completed in %.3fs, '
728 'expiration time of fetched cache %.1fs.',
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 log.debug('PLUGIN USER DATA: %s', plugin_user)
601 log.debug('PLUGIN USER DATA: %s', plugin_user)
732
602
@@ -735,5 +605,5 b' def authenticate(username, password, env'
735 return plugin_user
605 return plugin_user
736 # we failed to Auth because .auth() method didn't return proper user
606 # we failed to Auth because .auth() method didn't return proper user
737 log.debug("User `%s` failed to authenticate against %s",
607 log.debug("User `%s` failed to authenticate against %s",
738 display_user, plugin_id)
608 display_user, plugin.get_id())
739 return None
609 return None
@@ -34,6 +34,7 b' from sqlalchemy.ext.hybrid import hybrid'
34 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
39 from rhodecode.model.db import User
39
40
@@ -58,12 +59,14 b' class CrowdSettingsSchema(AuthnPluginSet'
58 colander.String(),
59 colander.String(),
59 default='127.0.0.1',
60 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 description=_('The FQDN or IP of the Atlassian CROWD Server'),
62 preparer=strip_whitespace,
61 title=_('Host'),
63 title=_('Host'),
62 widget='string')
64 widget='string')
63 port = colander.SchemaNode(
65 port = colander.SchemaNode(
64 colander.Int(),
66 colander.Int(),
65 default=8095,
67 default=8095,
66 description=_('The Port in use by the Atlassian CROWD Server'),
68 description=_('The Port in use by the Atlassian CROWD Server'),
69 preparer=strip_whitespace,
67 title=_('Port'),
70 title=_('Port'),
68 validator=colander.Range(min=0, max=65536),
71 validator=colander.Range(min=0, max=65536),
69 widget='int')
72 widget='int')
@@ -71,12 +74,14 b' class CrowdSettingsSchema(AuthnPluginSet'
71 colander.String(),
74 colander.String(),
72 default='',
75 default='',
73 description=_('The Application Name to authenticate to CROWD'),
76 description=_('The Application Name to authenticate to CROWD'),
77 preparer=strip_whitespace,
74 title=_('Application Name'),
78 title=_('Application Name'),
75 widget='string')
79 widget='string')
76 app_password = colander.SchemaNode(
80 app_password = colander.SchemaNode(
77 colander.String(),
81 colander.String(),
78 default='',
82 default='',
79 description=_('The password to authenticate to CROWD'),
83 description=_('The password to authenticate to CROWD'),
84 preparer=strip_whitespace,
80 title=_('Application Password'),
85 title=_('Application Password'),
81 widget='password')
86 widget='password')
82 admin_groups = colander.SchemaNode(
87 admin_groups = colander.SchemaNode(
@@ -85,6 +90,7 b' class CrowdSettingsSchema(AuthnPluginSet'
85 description=_('A comma separated list of group names that identify '
90 description=_('A comma separated list of group names that identify '
86 'users as RhodeCode Administrators'),
91 'users as RhodeCode Administrators'),
87 missing='',
92 missing='',
93 preparer=strip_whitespace,
88 title=_('Admin Groups'),
94 title=_('Admin Groups'),
89 widget='string')
95 widget='string')
90
96
@@ -191,12 +197,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
191 config.add_view(
197 config.add_view(
192 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
193 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
194 request_method='GET',
201 request_method='GET',
195 route_name='auth_home',
202 route_name='auth_home',
196 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
197 config.add_view(
204 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
200 request_method='POST',
208 request_method='POST',
201 route_name='auth_home',
209 route_name='auth_home',
202 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
@@ -36,6 +36,7 b' from sqlalchemy.ext.hybrid import hybrid'
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
39 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.model.db import User
41 from rhodecode.model.db import User
41
42
@@ -60,6 +61,7 b' class JasigCasSettingsSchema(AuthnPlugin'
60 colander.String(),
61 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
62 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
63 description=_('The url of the Jasig CAS REST service'),
64 preparer=strip_whitespace,
63 title=_('URL'),
65 title=_('URL'),
64 widget='string')
66 widget='string')
65
67
@@ -72,12 +74,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
72 config.add_view(
74 config.add_view(
73 'rhodecode.authentication.views.AuthnPluginViewBase',
75 'rhodecode.authentication.views.AuthnPluginViewBase',
74 attr='settings_get',
76 attr='settings_get',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
75 request_method='GET',
78 request_method='GET',
76 route_name='auth_home',
79 route_name='auth_home',
77 context=JasigCasAuthnResource)
80 context=JasigCasAuthnResource)
78 config.add_view(
81 config.add_view(
79 'rhodecode.authentication.views.AuthnPluginViewBase',
82 'rhodecode.authentication.views.AuthnPluginViewBase',
80 attr='settings_post',
83 attr='settings_post',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
81 request_method='POST',
85 request_method='POST',
82 route_name='auth_home',
86 route_name='auth_home',
83 context=JasigCasAuthnResource)
87 context=JasigCasAuthnResource)
@@ -92,8 +96,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
92 def name(self):
96 def name(self):
93 return "jasig-cas"
97 return "jasig-cas"
94
98
95 @hybrid_property
99 @property
96 def is_container_auth(self):
100 def is_headers_auth(self):
97 return True
101 return True
98
102
99 def use_fake_password(self):
103 def use_fake_password(self):
@@ -33,6 +33,7 b' from sqlalchemy.ext.hybrid import hybrid'
33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
39 )
@@ -45,8 +46,9 b' log = logging.getLogger(__name__)'
45 try:
46 try:
46 import ldap
47 import ldap
47 except ImportError:
48 except ImportError:
48 # means that python-ldap is not installed
49 # means that python-ldap is not installed, we use Missing object to mark
49 ldap = Missing()
50 # ldap lib is Missing
51 ldap = Missing
50
52
51
53
52 def plugin_factory(plugin_id, *args, **kwds):
54 def plugin_factory(plugin_id, *args, **kwds):
@@ -71,12 +73,14 b' class LdapSettingsSchema(AuthnPluginSett'
71 colander.String(),
73 colander.String(),
72 default='',
74 default='',
73 description=_('Host of the LDAP Server'),
75 description=_('Host of the LDAP Server'),
76 preparer=strip_whitespace,
74 title=_('LDAP Host'),
77 title=_('LDAP Host'),
75 widget='string')
78 widget='string')
76 port = colander.SchemaNode(
79 port = colander.SchemaNode(
77 colander.Int(),
80 colander.Int(),
78 default=389,
81 default=389,
79 description=_('Port that the LDAP server is listening on'),
82 description=_('Port that the LDAP server is listening on'),
83 preparer=strip_whitespace,
80 title=_('Port'),
84 title=_('Port'),
81 validator=colander.Range(min=0, max=65536),
85 validator=colander.Range(min=0, max=65536),
82 widget='int')
86 widget='int')
@@ -85,6 +89,7 b' class LdapSettingsSchema(AuthnPluginSett'
85 default='',
89 default='',
86 description=_('User to connect to LDAP'),
90 description=_('User to connect to LDAP'),
87 missing='',
91 missing='',
92 preparer=strip_whitespace,
88 title=_('Account'),
93 title=_('Account'),
89 widget='string')
94 widget='string')
90 dn_pass = colander.SchemaNode(
95 dn_pass = colander.SchemaNode(
@@ -92,6 +97,7 b' class LdapSettingsSchema(AuthnPluginSett'
92 default='',
97 default='',
93 description=_('Password to connect to LDAP'),
98 description=_('Password to connect to LDAP'),
94 missing='',
99 missing='',
100 preparer=strip_whitespace,
95 title=_('Password'),
101 title=_('Password'),
96 widget='password')
102 widget='password')
97 tls_kind = colander.SchemaNode(
103 tls_kind = colander.SchemaNode(
@@ -113,6 +119,7 b' class LdapSettingsSchema(AuthnPluginSett'
113 default='',
119 default='',
114 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
120 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
115 missing='',
121 missing='',
122 preparer=strip_whitespace,
116 title=_('Base DN'),
123 title=_('Base DN'),
117 widget='string')
124 widget='string')
118 filter = colander.SchemaNode(
125 filter = colander.SchemaNode(
@@ -120,6 +127,7 b' class LdapSettingsSchema(AuthnPluginSett'
120 default='',
127 default='',
121 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
128 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
122 missing='',
129 missing='',
130 preparer=strip_whitespace,
123 title=_('LDAP Search Filter'),
131 title=_('LDAP Search Filter'),
124 widget='string')
132 widget='string')
125 search_scope = colander.SchemaNode(
133 search_scope = colander.SchemaNode(
@@ -133,14 +141,16 b' class LdapSettingsSchema(AuthnPluginSett'
133 colander.String(),
141 colander.String(),
134 default='',
142 default='',
135 description=_('LDAP Attribute to map to user name'),
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 title=_('Login Attribute'),
146 title=_('Login Attribute'),
137 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
138 widget='string')
147 widget='string')
139 attr_firstname = colander.SchemaNode(
148 attr_firstname = colander.SchemaNode(
140 colander.String(),
149 colander.String(),
141 default='',
150 default='',
142 description=_('LDAP Attribute to map to first name'),
151 description=_('LDAP Attribute to map to first name'),
143 missing='',
152 missing='',
153 preparer=strip_whitespace,
144 title=_('First Name Attribute'),
154 title=_('First Name Attribute'),
145 widget='string')
155 widget='string')
146 attr_lastname = colander.SchemaNode(
156 attr_lastname = colander.SchemaNode(
@@ -148,6 +158,7 b' class LdapSettingsSchema(AuthnPluginSett'
148 default='',
158 default='',
149 description=_('LDAP Attribute to map to last name'),
159 description=_('LDAP Attribute to map to last name'),
150 missing='',
160 missing='',
161 preparer=strip_whitespace,
151 title=_('Last Name Attribute'),
162 title=_('Last Name Attribute'),
152 widget='string')
163 widget='string')
153 attr_email = colander.SchemaNode(
164 attr_email = colander.SchemaNode(
@@ -155,6 +166,7 b' class LdapSettingsSchema(AuthnPluginSett'
155 default='',
166 default='',
156 description=_('LDAP Attribute to map to email address'),
167 description=_('LDAP Attribute to map to email address'),
157 missing='',
168 missing='',
169 preparer=strip_whitespace,
158 title=_('Email Attribute'),
170 title=_('Email Attribute'),
159 widget='string')
171 widget='string')
160
172
@@ -171,7 +183,7 b' class AuthLdap(object):'
171 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
183 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
172 search_scope='SUBTREE', attr_login='uid',
184 search_scope='SUBTREE', attr_login='uid',
173 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
185 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
174 if isinstance(ldap, Missing):
186 if ldap == Missing:
175 raise LdapImportError("Missing or incompatible ldap library")
187 raise LdapImportError("Missing or incompatible ldap library")
176
188
177 self.ldap_version = ldap_version
189 self.ldap_version = ldap_version
@@ -317,12 +329,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
317 config.add_view(
329 config.add_view(
318 'rhodecode.authentication.views.AuthnPluginViewBase',
330 'rhodecode.authentication.views.AuthnPluginViewBase',
319 attr='settings_get',
331 attr='settings_get',
332 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
320 request_method='GET',
333 request_method='GET',
321 route_name='auth_home',
334 route_name='auth_home',
322 context=LdapAuthnResource)
335 context=LdapAuthnResource)
323 config.add_view(
336 config.add_view(
324 'rhodecode.authentication.views.AuthnPluginViewBase',
337 'rhodecode.authentication.views.AuthnPluginViewBase',
325 attr='settings_post',
338 attr='settings_post',
339 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
326 request_method='POST',
340 request_method='POST',
327 route_name='auth_home',
341 route_name='auth_home',
328 context=LdapAuthnResource)
342 context=LdapAuthnResource)
@@ -35,6 +35,7 b' from sqlalchemy.ext.hybrid import hybrid'
35 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
@@ -57,6 +58,7 b' class PamSettingsSchema(AuthnPluginSetti'
57 colander.String(),
58 colander.String(),
58 default='login',
59 default='login',
59 description=_('PAM service name to use for authentication.'),
60 description=_('PAM service name to use for authentication.'),
61 preparer=strip_whitespace,
60 title=_('PAM service name'),
62 title=_('PAM service name'),
61 widget='string')
63 widget='string')
62 gecos = colander.SchemaNode(
64 gecos = colander.SchemaNode(
@@ -64,6 +66,7 b' class PamSettingsSchema(AuthnPluginSetti'
64 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
65 description=_('Regular expression for extracting user name/email etc. '
67 description=_('Regular expression for extracting user name/email etc. '
66 'from Unix userinfo.'),
68 'from Unix userinfo.'),
69 preparer=strip_whitespace,
67 title=_('Gecos Regex'),
70 title=_('Gecos Regex'),
68 widget='string')
71 widget='string')
69
72
@@ -79,12 +82,14 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
79 config.add_view(
82 config.add_view(
80 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
81 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
82 request_method='GET',
86 request_method='GET',
83 route_name='auth_home',
87 route_name='auth_home',
84 context=PamAuthnResource)
88 context=PamAuthnResource)
85 config.add_view(
89 config.add_view(
86 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
87 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
88 request_method='POST',
93 request_method='POST',
89 route_name='auth_home',
94 route_name='auth_home',
90 context=PamAuthnResource)
95 context=PamAuthnResource)
@@ -52,12 +52,14 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
52 config.add_view(
52 config.add_view(
53 'rhodecode.authentication.views.AuthnPluginViewBase',
53 'rhodecode.authentication.views.AuthnPluginViewBase',
54 attr='settings_get',
54 attr='settings_get',
55 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
55 request_method='GET',
56 request_method='GET',
56 route_name='auth_home',
57 route_name='auth_home',
57 context=RhodecodeAuthnResource)
58 context=RhodecodeAuthnResource)
58 config.add_view(
59 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_post',
61 attr='settings_post',
62 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
61 request_method='POST',
63 request_method='POST',
62 route_name='auth_home',
64 route_name='auth_home',
63 context=RhodecodeAuthnResource)
65 context=RhodecodeAuthnResource)
@@ -25,14 +25,20 b' from zope.interface import implementer'
25
25
26 from rhodecode.authentication.interface import IAuthnPluginRegistry
26 from rhodecode.authentication.interface import IAuthnPluginRegistry
27 from rhodecode.lib.utils2 import safe_str
27 from rhodecode.lib.utils2 import safe_str
28 from rhodecode.model.settings import SettingsModel
28
29
29 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
30
31
31
32
32 @implementer(IAuthnPluginRegistry)
33 @implementer(IAuthnPluginRegistry)
33 class AuthenticationPluginRegistry(object):
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 self._plugins = {}
40 self._plugins = {}
41 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
36
42
37 def add_authn_plugin(self, config, plugin):
43 def add_authn_plugin(self, config, plugin):
38 plugin_id = plugin.get_id()
44 plugin_id = plugin.get_id()
@@ -51,3 +57,31 b' class AuthenticationPluginRegistry(objec'
51
57
52 def get_plugin(self, plugin_id):
58 def get_plugin(self, plugin_id):
53 return self._plugins.get(plugin_id, None)
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 import logging
21 import logging
22
22
23 from pyramid.exceptions import ConfigurationError
23 from pyramid.exceptions import ConfigurationError
24 from pyramid.i18n import TranslationStringFactory
25
24
26 from rhodecode.lib.utils2 import safe_str
25 from rhodecode.lib.utils2 import safe_str
27 from rhodecode.model.settings import SettingsModel
26 from rhodecode.model.settings import SettingsModel
27 from rhodecode.translation import _
28
28
29 _ = TranslationStringFactory('rhodecode-enterprise')
30
29
31 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
32
31
@@ -128,7 +127,7 b' class AuthnRootResource(AuthnResourceBas'
128 # Allow plugin resources with identical names by rename duplicates.
127 # Allow plugin resources with identical names by rename duplicates.
129 unique_name = _ensure_unique_name(resource.__name__)
128 unique_name = _ensure_unique_name(resource.__name__)
130 if unique_name != resource.__name__:
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 'by authentication plugin "%s"', resource.__name__,
131 'by authentication plugin "%s"', resource.__name__,
133 plugin_id)
132 plugin_id)
134 resource.__name__ = unique_name
133 resource.__name__ = unique_name
@@ -20,9 +20,7 b''
20
20
21 import colander
21 import colander
22
22
23 from pyramid.i18n import TranslationStringFactory
23 from rhodecode.translation import _
24
25 _ = TranslationStringFactory('rhodecode-enterprise')
26
24
27
25
28 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
@@ -23,7 +23,6 b' import formencode.htmlfill'
23 import logging
23 import logging
24
24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.i18n import TranslationStringFactory
27 from pyramid.renderers import render
26 from pyramid.renderers import render
28 from pyramid.response import Response
27 from pyramid.response import Response
29
28
@@ -34,11 +33,10 b' from rhodecode.lib.auth import LoginRequ'
34 from rhodecode.model.forms import AuthSettingsForm
33 from rhodecode.model.forms import AuthSettingsForm
35 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
36 from rhodecode.model.settings import SettingsModel
35 from rhodecode.model.settings import SettingsModel
36 from rhodecode.translation import _
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40 _ = TranslationStringFactory('rhodecode-enterprise')
41
42
40
43 class AuthnPluginViewBase(object):
41 class AuthnPluginViewBase(object):
44
42
@@ -47,51 +45,27 b' class AuthnPluginViewBase(object):'
47 self.context = context
45 self.context = context
48 self.plugin = context.plugin
46 self.plugin = context.plugin
49
47
50 # TODO: Think about replacing the htmlfill stuff.
48 def settings_get(self, defaults=None, errors=None):
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):
72 """
49 """
73 View that displays the plugin settings as a form.
50 View that displays the plugin settings as a form.
74 """
51 """
75 form_defaults = {}
52 defaults = defaults or {}
76 validation_errors = None
53 errors = errors or {}
77 schema = self.plugin.get_settings_schema()
54 schema = self.plugin.get_settings_schema()
78
55
79 # Get default values for the form.
56 # Get default values for the form.
80 for node in schema.children:
57 for node in schema:
81 value = self.plugin.get_setting_by_name(node.name) or node.default
58 db_value = self.plugin.get_setting_by_name(node.name)
82 form_defaults[node.name] = value
59 defaults.setdefault(node.name, db_value)
83
60
84 template_context = {
61 template_context = {
62 'defaults': defaults,
63 'errors': errors,
64 'plugin': self.context.plugin,
85 'resource': self.context,
65 'resource': self.context,
86 'plugin': self.context.plugin
87 }
66 }
88
67
89 return Response(self._render_and_fill(
68 return template_context
90 'rhodecode:templates/admin/auth/plugin_settings.html',
91 template_context,
92 self.request,
93 form_defaults,
94 validation_errors))
95
69
96 def settings_post(self):
70 def settings_post(self):
97 """
71 """
@@ -102,24 +76,12 b' class AuthnPluginViewBase(object):'
102 valid_data = schema.deserialize(self.request.params)
76 valid_data = schema.deserialize(self.request.params)
103 except colander.Invalid, e:
77 except colander.Invalid, e:
104 # Display error message and display form again.
78 # Display error message and display form again.
105 form_defaults = self.request.params
106 validation_errors = e.asdict()
107 self.request.session.flash(
79 self.request.session.flash(
108 _('Errors exist when saving plugin settings. '
80 _('Errors exist when saving plugin settings. '
109 'Please check the form inputs.'),
81 'Please check the form inputs.'),
110 queue='error')
82 queue='error')
111
83 defaults = schema.flatten(self.request.params)
112 template_context = {
84 return self.settings_get(errors=e.asdict(), defaults=defaults)
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))
123
85
124 # Store validated data.
86 # Store validated data.
125 for name, value in valid_data.items():
87 for name, value in valid_data.items():
@@ -151,10 +113,10 b' class AuthSettingsView(object):'
151
113
152 @LoginRequired()
114 @LoginRequired()
153 @HasPermissionAllDecorator('hg.admin')
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 authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry)
118 authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry)
156 default_plugins = ['egg:rhodecode-enterprise-ce#rhodecode']
119 enabled_plugins = SettingsModel().get_auth_plugins()
157 enabled_plugins = SettingsModel().get_auth_plugins() or default_plugins
158
120
159 # Create template context and render it.
121 # Create template context and render it.
160 template_context = {
122 template_context = {
@@ -27,10 +27,12 b' import logging'
27 import rhodecode
27 import rhodecode
28 import platform
28 import platform
29 import re
29 import re
30 import io
30
31
31 from mako.lookup import TemplateLookup
32 from mako.lookup import TemplateLookup
32 from pylons.configuration import PylonsConfig
33 from pylons.configuration import PylonsConfig
33 from pylons.error import handle_mako_error
34 from pylons.error import handle_mako_error
35 from pyramid.settings import asbool
34
36
35 # don't remove this import it does magic for celery
37 # don't remove this import it does magic for celery
36 from rhodecode.lib import celerypylons # noqa
38 from rhodecode.lib import celerypylons # noqa
@@ -39,6 +41,7 b' import rhodecode.lib.app_globals as app_'
39
41
40 from rhodecode.config import utils
42 from rhodecode.config import utils
41 from rhodecode.config.routing import make_map
43 from rhodecode.config.routing import make_map
44 from rhodecode.config.jsroutes import generate_jsroutes_content
42
45
43 from rhodecode.lib import helpers
46 from rhodecode.lib import helpers
44 from rhodecode.lib.auth import set_available_permissions
47 from rhodecode.lib.auth import set_available_permissions
@@ -51,7 +54,6 b' from rhodecode.model.scm import ScmModel'
51
54
52 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
53
56
54
55 def load_environment(global_conf, app_conf, initial=False,
57 def load_environment(global_conf, app_conf, initial=False,
56 test_env=None, test_index=None):
58 test_env=None, test_index=None):
57 """
59 """
@@ -60,7 +62,6 b' def load_environment(global_conf, app_co'
60 """
62 """
61 config = PylonsConfig()
63 config = PylonsConfig()
62
64
63 rhodecode.is_test = str2bool(app_conf.get('is_test', 'False'))
64
65
65 # Pylons paths
66 # Pylons paths
66 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
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 config['app_conf'].get('celery.always.eager'))
81 config['app_conf'].get('celery.always.eager'))
81
82
82 config['routes.map'] = make_map(config)
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 config['pylons.app_globals'] = app_globals.Globals(config)
94 config['pylons.app_globals'] = app_globals.Globals(config)
84 config['pylons.h'] = helpers
95 config['pylons.h'] = helpers
85 rhodecode.CONFIG = config
96 rhodecode.CONFIG = config
@@ -100,18 +111,6 b' def load_environment(global_conf, app_co'
100
111
101 # sets the c attribute access when don't existing attribute are accessed
112 # sets the c attribute access when don't existing attribute are accessed
102 config['pylons.strict_tmpl_context'] = True
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 # Limit backends to "vcs.backends" from configuration
115 # Limit backends to "vcs.backends" from configuration
117 backends = config['vcs.backends'] = aslist(
116 backends = config['vcs.backends'] = aslist(
@@ -133,10 +132,6 b' def load_environment(global_conf, app_co'
133 protocol=utils.get_vcs_server_protocol(config),
132 protocol=utils.get_vcs_server_protocol(config),
134 log_level=config['vcs.server.log_level'])
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 set_available_permissions(config)
135 set_available_permissions(config)
141 db_cfg = make_db_config(clear_session=True)
136 db_cfg = make_db_config(clear_session=True)
142
137
@@ -179,3 +174,19 b' def _use_direct_hook_calls(config):'
179 def _get_vcs_hooks_protocol(config):
174 def _get_vcs_hooks_protocol(config):
180 protocol = config.get('vcs.hooks.protocol', 'pyro4').lower()
175 protocol = config.get('vcs.hooks.protocol', 'pyro4').lower()
181 return protocol
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 import rhodecode
38 import rhodecode
39 from rhodecode.config import patches
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 from rhodecode.lib.middleware import csrf
42 from rhodecode.lib.middleware import csrf
42 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
43 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
43 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
44 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
@@ -160,6 +161,9 b' def make_pyramid_app(global_config, **se'
160 sanitize_settings_and_apply_defaults(settings)
161 sanitize_settings_and_apply_defaults(settings)
161 config = Configurator(settings=settings)
162 config = Configurator(settings=settings)
162 add_pylons_compat_data(config.registry, global_config, settings_pylons)
163 add_pylons_compat_data(config.registry, global_config, settings_pylons)
164
165 load_pyramid_environment(global_config, settings)
166
163 includeme(config)
167 includeme(config)
164 includeme_last(config)
168 includeme_last(config)
165 pyramid_app = config.make_wsgi_app()
169 pyramid_app = config.make_wsgi_app()
@@ -182,6 +186,7 b' def includeme(config):'
182 config.include('pyramid_mako')
186 config.include('pyramid_mako')
183 config.include('pyramid_beaker')
187 config.include('pyramid_beaker')
184 config.include('rhodecode.authentication')
188 config.include('rhodecode.authentication')
189 config.include('rhodecode.login')
185 config.include('rhodecode.tweens')
190 config.include('rhodecode.tweens')
186 config.include('rhodecode.api')
191 config.include('rhodecode.api')
187
192
@@ -301,6 +306,7 b' def sanitize_settings_and_apply_defaults'
301
306
302 _bool_setting(settings, 'vcs.server.enable', 'true')
307 _bool_setting(settings, 'vcs.server.enable', 'true')
303 _bool_setting(settings, 'static_files', 'true')
308 _bool_setting(settings, 'static_files', 'true')
309 _bool_setting(settings, 'is_test', 'false')
304
310
305 return settings
311 return settings
306
312
@@ -29,6 +29,7 b' IMPORTANT: if you change any routing her'
29 and _route_name variable which uses some of stored naming here to do redirects.
29 and _route_name variable which uses some of stored naming here to do redirects.
30 """
30 """
31 import os
31 import os
32 import re
32 from routes import Mapper
33 from routes import Mapper
33
34
34 from rhodecode.config import routing_links
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 def make_map(config):
105 def make_map(config):
54 """Create, configure and return the routes Mapper"""
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 always_scan=config['debug'])
108 always_scan=config['debug'])
57 rmap.minimization = False
109 rmap.minimization = False
58 rmap.explicit = False
110 rmap.explicit = False
@@ -124,14 +176,14 b' def make_map(config):'
124 #==========================================================================
176 #==========================================================================
125
177
126 # MAIN PAGE
178 # MAIN PAGE
127 rmap.connect('home', '/', controller='home', action='index')
179 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
128 rmap.connect('repo_switcher_data', '/_repos_and_groups', controller='home',
180 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
129 action='repo_switcher_data')
181 action='goto_switcher_data')
130 rmap.connect('repo_list_data', '/_repos', controller='home',
182 rmap.connect('repo_list_data', '/_repos', controller='home',
131 action='repo_list_data')
183 action='repo_list_data')
132
184
133 rmap.connect('user_autocomplete_data', '/_users', controller='home',
185 rmap.connect('user_autocomplete_data', '/_users', controller='home',
134 action='user_autocomplete_data')
186 action='user_autocomplete_data', jsroute=True)
135 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
187 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
136 action='user_group_autocomplete_data')
188 action='user_group_autocomplete_data')
137
189
@@ -167,7 +219,7 b' def make_map(config):'
167 action='create', conditions={'method': ['POST']})
219 action='create', conditions={'method': ['POST']})
168 m.connect('repos', '/repos',
220 m.connect('repos', '/repos',
169 action='index', conditions={'method': ['GET']})
221 action='index', conditions={'method': ['GET']})
170 m.connect('new_repo', '/create_repository',
222 m.connect('new_repo', '/create_repository', jsroute=True,
171 action='create_repository', conditions={'method': ['GET']})
223 action='create_repository', conditions={'method': ['GET']})
172 m.connect('/repos/{repo_name}',
224 m.connect('/repos/{repo_name}',
173 action='update', conditions={'method': ['PUT'],
225 action='update', conditions={'method': ['PUT'],
@@ -303,22 +355,29 b' def make_map(config):'
303 function=check_user_group)
355 function=check_user_group)
304
356
305 # EXTRAS USER GROUP ROUTES
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 action='edit_global_perms', conditions={'method': ['GET']})
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 action='update_global_perms', conditions={'method': ['PUT']})
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 action='edit_perms_summary', conditions={'method': ['GET']})
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 action='edit_perms', conditions={'method': ['GET']})
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 action='update_perms', conditions={'method': ['PUT']})
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 action='edit_advanced', conditions={'method': ['GET']})
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 action='edit_members', conditions={'method': ['GET']})
381 action='edit_members', conditions={'method': ['GET']})
323
382
324 # ADMIN PERMISSIONS ROUTES
383 # ADMIN PERMISSIONS ROUTES
@@ -496,12 +555,6 b' def make_map(config):'
496 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
555 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
497 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
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 # NOTIFICATION REST ROUTES
558 # NOTIFICATION REST ROUTES
506 with rmap.submapper(path_prefix=ADMIN_PREFIX,
559 with rmap.submapper(path_prefix=ADMIN_PREFIX,
507 controller='admin/notifications') as m:
560 controller='admin/notifications') as m:
@@ -522,9 +575,9 b' def make_map(config):'
522 controller='admin/gists') as m:
575 controller='admin/gists') as m:
523 m.connect('gists', '/gists',
576 m.connect('gists', '/gists',
524 action='create', conditions={'method': ['POST']})
577 action='create', conditions={'method': ['POST']})
525 m.connect('gists', '/gists',
578 m.connect('gists', '/gists', jsroute=True,
526 action='index', conditions={'method': ['GET']})
579 action='index', conditions={'method': ['GET']})
527 m.connect('new_gist', '/gists/new',
580 m.connect('new_gist', '/gists/new', jsroute=True,
528 action='new', conditions={'method': ['GET']})
581 action='new', conditions={'method': ['GET']})
529
582
530 m.connect('/gists/{gist_id}',
583 m.connect('/gists/{gist_id}',
@@ -557,8 +610,12 b' def make_map(config):'
557 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
610 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
558 action='add_repo')
611 action='add_repo')
559 m.connect(
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 action='pull_requests')
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 # USER JOURNAL
620 # USER JOURNAL
564 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
621 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
@@ -586,7 +643,7 b' def make_map(config):'
586 action='public_journal_atom')
643 action='public_journal_atom')
587
644
588 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
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 conditions={'method': ['POST']})
647 conditions={'method': ['POST']})
591
648
592 # FULL TEXT SEARCH
649 # FULL TEXT SEARCH
@@ -598,27 +655,6 b' def make_map(config):'
598 conditions={'function': check_repo},
655 conditions={'function': check_repo},
599 requirements=URL_NAME_REQUIREMENTS)
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 # FEEDS
658 # FEEDS
623 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
659 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
624 controller='feed', action='rss',
660 controller='feed', action='rss',
@@ -644,17 +680,17 b' def make_map(config):'
644 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
680 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
645 controller='summary', action='repo_stats',
681 controller='summary', action='repo_stats',
646 conditions={'function': check_repo},
682 conditions={'function': check_repo},
647 requirements=URL_NAME_REQUIREMENTS)
683 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
648
684
649 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
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 requirements=URL_NAME_REQUIREMENTS)
687 requirements=URL_NAME_REQUIREMENTS)
652 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
688 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
653 controller='summary', action='repo_refs_changelog_data',
689 controller='summary', action='repo_refs_changelog_data',
654 requirements=URL_NAME_REQUIREMENTS)
690 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
655
691
656 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
692 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
657 controller='changeset', revision='tip',
693 controller='changeset', revision='tip', jsroute=True,
658 conditions={'function': check_repo},
694 conditions={'function': check_repo},
659 requirements=URL_NAME_REQUIREMENTS)
695 requirements=URL_NAME_REQUIREMENTS)
660 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
696 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
@@ -667,12 +703,13 b' def make_map(config):'
667 requirements=URL_NAME_REQUIREMENTS)
703 requirements=URL_NAME_REQUIREMENTS)
668
704
669 # repo edit options
705 # repo edit options
670 rmap.connect('edit_repo', '/{repo_name}/settings',
706 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
671 controller='admin/repos', action='edit',
707 controller='admin/repos', action='edit',
672 conditions={'method': ['GET'], 'function': check_repo},
708 conditions={'method': ['GET'], 'function': check_repo},
673 requirements=URL_NAME_REQUIREMENTS)
709 requirements=URL_NAME_REQUIREMENTS)
674
710
675 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
711 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
712 jsroute=True,
676 controller='admin/repos', action='edit_permissions',
713 controller='admin/repos', action='edit_permissions',
677 conditions={'method': ['GET'], 'function': check_repo},
714 conditions={'method': ['GET'], 'function': check_repo},
678 requirements=URL_NAME_REQUIREMENTS)
715 requirements=URL_NAME_REQUIREMENTS)
@@ -804,13 +841,13 b' def make_map(config):'
804 requirements=URL_NAME_REQUIREMENTS)
841 requirements=URL_NAME_REQUIREMENTS)
805
842
806 rmap.connect('changeset_comment',
843 rmap.connect('changeset_comment',
807 '/{repo_name}/changeset/{revision}/comment',
844 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
808 controller='changeset', revision='tip', action='comment',
845 controller='changeset', revision='tip', action='comment',
809 conditions={'function': check_repo},
846 conditions={'function': check_repo},
810 requirements=URL_NAME_REQUIREMENTS)
847 requirements=URL_NAME_REQUIREMENTS)
811
848
812 rmap.connect('changeset_comment_preview',
849 rmap.connect('changeset_comment_preview',
813 '/{repo_name}/changeset/comment/preview',
850 '/{repo_name}/changeset/comment/preview', jsroute=True,
814 controller='changeset', action='preview_comment',
851 controller='changeset', action='preview_comment',
815 conditions={'function': check_repo, 'method': ['POST']},
852 conditions={'function': check_repo, 'method': ['POST']},
816 requirements=URL_NAME_REQUIREMENTS)
853 requirements=URL_NAME_REQUIREMENTS)
@@ -819,11 +856,11 b' def make_map(config):'
819 '/{repo_name}/changeset/comment/{comment_id}/delete',
856 '/{repo_name}/changeset/comment/{comment_id}/delete',
820 controller='changeset', action='delete_comment',
857 controller='changeset', action='delete_comment',
821 conditions={'function': check_repo, 'method': ['DELETE']},
858 conditions={'function': check_repo, 'method': ['DELETE']},
822 requirements=URL_NAME_REQUIREMENTS)
859 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
823
860
824 rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}',
861 rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}',
825 controller='changeset', action='changeset_info',
862 controller='changeset', action='changeset_info',
826 requirements=URL_NAME_REQUIREMENTS)
863 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
827
864
828 rmap.connect('compare_home',
865 rmap.connect('compare_home',
829 '/{repo_name}/compare',
866 '/{repo_name}/compare',
@@ -835,33 +872,33 b' def make_map(config):'
835 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
872 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
836 controller='compare', action='compare',
873 controller='compare', action='compare',
837 conditions={'function': check_repo},
874 conditions={'function': check_repo},
838 requirements=URL_NAME_REQUIREMENTS)
875 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
839
876
840 rmap.connect('pullrequest_home',
877 rmap.connect('pullrequest_home',
841 '/{repo_name}/pull-request/new', controller='pullrequests',
878 '/{repo_name}/pull-request/new', controller='pullrequests',
842 action='index', conditions={'function': check_repo,
879 action='index', conditions={'function': check_repo,
843 'method': ['GET']},
880 'method': ['GET']},
844 requirements=URL_NAME_REQUIREMENTS)
881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
845
882
846 rmap.connect('pullrequest',
883 rmap.connect('pullrequest',
847 '/{repo_name}/pull-request/new', controller='pullrequests',
884 '/{repo_name}/pull-request/new', controller='pullrequests',
848 action='create', conditions={'function': check_repo,
885 action='create', conditions={'function': check_repo,
849 'method': ['POST']},
886 'method': ['POST']},
850 requirements=URL_NAME_REQUIREMENTS)
887 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
851
888
852 rmap.connect('pullrequest_repo_refs',
889 rmap.connect('pullrequest_repo_refs',
853 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
890 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
854 controller='pullrequests',
891 controller='pullrequests',
855 action='get_repo_refs',
892 action='get_repo_refs',
856 conditions={'function': check_repo, 'method': ['GET']},
893 conditions={'function': check_repo, 'method': ['GET']},
857 requirements=URL_NAME_REQUIREMENTS)
894 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
858
895
859 rmap.connect('pullrequest_repo_destinations',
896 rmap.connect('pullrequest_repo_destinations',
860 '/{repo_name}/pull-request/repo-destinations',
897 '/{repo_name}/pull-request/repo-destinations',
861 controller='pullrequests',
898 controller='pullrequests',
862 action='get_repo_destinations',
899 action='get_repo_destinations',
863 conditions={'function': check_repo, 'method': ['GET']},
900 conditions={'function': check_repo, 'method': ['GET']},
864 requirements=URL_NAME_REQUIREMENTS)
901 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
865
902
866 rmap.connect('pullrequest_show',
903 rmap.connect('pullrequest_show',
867 '/{repo_name}/pull-request/{pull_request_id}',
904 '/{repo_name}/pull-request/{pull_request_id}',
@@ -875,7 +912,7 b' def make_map(config):'
875 controller='pullrequests',
912 controller='pullrequests',
876 action='update', conditions={'function': check_repo,
913 action='update', conditions={'function': check_repo,
877 'method': ['PUT']},
914 'method': ['PUT']},
878 requirements=URL_NAME_REQUIREMENTS)
915 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
879
916
880 rmap.connect('pullrequest_merge',
917 rmap.connect('pullrequest_merge',
881 '/{repo_name}/pull-request/{pull_request_id}',
918 '/{repo_name}/pull-request/{pull_request_id}',
@@ -896,20 +933,20 b' def make_map(config):'
896 controller='pullrequests',
933 controller='pullrequests',
897 action='show_all', conditions={'function': check_repo,
934 action='show_all', conditions={'function': check_repo,
898 'method': ['GET']},
935 'method': ['GET']},
899 requirements=URL_NAME_REQUIREMENTS)
936 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900
937
901 rmap.connect('pullrequest_comment',
938 rmap.connect('pullrequest_comment',
902 '/{repo_name}/pull-request-comment/{pull_request_id}',
939 '/{repo_name}/pull-request-comment/{pull_request_id}',
903 controller='pullrequests',
940 controller='pullrequests',
904 action='comment', conditions={'function': check_repo,
941 action='comment', conditions={'function': check_repo,
905 'method': ['POST']},
942 'method': ['POST']},
906 requirements=URL_NAME_REQUIREMENTS)
943 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
907
944
908 rmap.connect('pullrequest_comment_delete',
945 rmap.connect('pullrequest_comment_delete',
909 '/{repo_name}/pull-request-comment/{comment_id}/delete',
946 '/{repo_name}/pull-request-comment/{comment_id}/delete',
910 controller='pullrequests', action='delete_comment',
947 controller='pullrequests', action='delete_comment',
911 conditions={'function': check_repo, 'method': ['DELETE']},
948 conditions={'function': check_repo, 'method': ['DELETE']},
912 requirements=URL_NAME_REQUIREMENTS)
949 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913
950
914 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
951 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
915 controller='summary', conditions={'function': check_repo},
952 controller='summary', conditions={'function': check_repo},
@@ -927,7 +964,7 b' def make_map(config):'
927 controller='bookmarks', conditions={'function': check_repo},
964 controller='bookmarks', conditions={'function': check_repo},
928 requirements=URL_NAME_REQUIREMENTS)
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 controller='changelog', conditions={'function': check_repo},
968 controller='changelog', conditions={'function': check_repo},
932 requirements=URL_NAME_REQUIREMENTS)
969 requirements=URL_NAME_REQUIREMENTS)
933
970
@@ -936,21 +973,21 b' def make_map(config):'
936 conditions={'function': check_repo},
973 conditions={'function': check_repo},
937 requirements=URL_NAME_REQUIREMENTS)
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 controller='changelog', f_path=None,
978 controller='changelog', f_path=None,
941 conditions={'function': check_repo},
979 conditions={'function': check_repo},
942 requirements=URL_NAME_REQUIREMENTS)
980 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
943
981
944 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
982 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
945 controller='changelog', action='changelog_details',
983 controller='changelog', action='changelog_details',
946 conditions={'function': check_repo},
984 conditions={'function': check_repo},
947 requirements=URL_NAME_REQUIREMENTS)
985 requirements=URL_NAME_REQUIREMENTS)
948
986
949 rmap.connect('files_home',
987 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
950 '/{repo_name}/files/{revision}/{f_path}',
951 controller='files', revision='tip', f_path='',
988 controller='files', revision='tip', f_path='',
952 conditions={'function': check_repo},
989 conditions={'function': check_repo},
953 requirements=URL_NAME_REQUIREMENTS)
990 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
954
991
955 rmap.connect('files_home_simple_catchrev',
992 rmap.connect('files_home_simple_catchrev',
956 '/{repo_name}/files/{revision}',
993 '/{repo_name}/files/{revision}',
@@ -968,13 +1005,13 b' def make_map(config):'
968 '/{repo_name}/history/{revision}/{f_path}',
1005 '/{repo_name}/history/{revision}/{f_path}',
969 controller='files', action='history', revision='tip', f_path='',
1006 controller='files', action='history', revision='tip', f_path='',
970 conditions={'function': check_repo},
1007 conditions={'function': check_repo},
971 requirements=URL_NAME_REQUIREMENTS)
1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
972
1009
973 rmap.connect('files_authors_home',
1010 rmap.connect('files_authors_home',
974 '/{repo_name}/authors/{revision}/{f_path}',
1011 '/{repo_name}/authors/{revision}/{f_path}',
975 controller='files', action='authors', revision='tip', f_path='',
1012 controller='files', action='authors', revision='tip', f_path='',
976 conditions={'function': check_repo},
1013 conditions={'function': check_repo},
977 requirements=URL_NAME_REQUIREMENTS)
1014 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
978
1015
979 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1016 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
980 controller='files', action='diff', f_path='',
1017 controller='files', action='diff', f_path='',
@@ -1053,19 +1090,19 b' def make_map(config):'
1053 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1090 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1054 controller='files', action='archivefile',
1091 controller='files', action='archivefile',
1055 conditions={'function': check_repo},
1092 conditions={'function': check_repo},
1056 requirements=URL_NAME_REQUIREMENTS)
1093 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1057
1094
1058 rmap.connect('files_nodelist_home',
1095 rmap.connect('files_nodelist_home',
1059 '/{repo_name}/nodelist/{revision}/{f_path}',
1096 '/{repo_name}/nodelist/{revision}/{f_path}',
1060 controller='files', action='nodelist',
1097 controller='files', action='nodelist',
1061 conditions={'function': check_repo},
1098 conditions={'function': check_repo},
1062 requirements=URL_NAME_REQUIREMENTS)
1099 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1063
1100
1064 rmap.connect('files_metadata_list_home',
1101 rmap.connect('files_metadata_list_home',
1065 '/{repo_name}/metadata_list/{revision}/{f_path}',
1102 '/{repo_name}/metadata_list/{revision}/{f_path}',
1066 controller='files', action='metadata_list',
1103 controller='files', action='metadata_list',
1067 conditions={'function': check_repo},
1104 conditions={'function': check_repo},
1068 requirements=URL_NAME_REQUIREMENTS)
1105 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1069
1106
1070 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1107 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1071 controller='forks', action='fork_create',
1108 controller='forks', action='fork_create',
@@ -1096,7 +1133,7 b' def make_map(config):'
1096
1133
1097 # catch all, at the end
1134 # catch all, at the end
1098 _connect_with_slash(
1135 _connect_with_slash(
1099 rmap, 'summary_home', '/{repo_name}',
1136 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1100 controller='summary', action='index',
1137 controller='summary', action='index',
1101 conditions={'function': check_repo},
1138 conditions={'function': check_repo},
1102 requirements=URL_NAME_REQUIREMENTS)
1139 requirements=URL_NAME_REQUIREMENTS)
@@ -73,6 +73,18 b' def initialize_database(config):'
73 init_model(engine, encryption_key=config['beaker.session.secret'])
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 def get_vcs_server_protocol(config):
88 def get_vcs_server_protocol(config):
77 protocol = config.get('vcs.server.protocol', 'pyro4')
89 protocol = config.get('vcs.server.protocol', 'pyro4')
78 return protocol
90 return protocol
@@ -39,16 +39,15 b' from rhodecode.lib.auth import ('
39 from rhodecode.lib.base import BaseController, render
39 from rhodecode.lib.base import BaseController, render
40 from rhodecode.lib.utils2 import safe_int, md5
40 from rhodecode.lib.utils2 import safe_int, md5
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.model.db import (Repository, PullRequest, PullRequestReviewers,
42 from rhodecode.model.db import (
43 UserEmailMap, User, UserFollowing,
43 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
44 ExternalIdentity)
44 UserFollowing)
45 from rhodecode.model.forms import UserForm, PasswordChangeForm
45 from rhodecode.model.forms import UserForm, PasswordChangeForm
46 from rhodecode.model.scm import RepoList
46 from rhodecode.model.scm import RepoList
47 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
48 from rhodecode.model.repo import RepoModel
48 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.settings import SettingsModel
52
51
53 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
54
53
@@ -347,27 +346,3 b' class MyAccountController(BaseController'
347 h.flash(_("Auth token successfully deleted"), category='success')
346 h.flash(_("Auth token successfully deleted"), category='success')
348
347
349 return redirect(url('my_account_auth_tokens'))
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 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.exceptions import UserGroupAssignedException,\
37 from rhodecode.lib.exceptions import UserGroupAssignedException,\
38 RepoGroupAssignmentError
38 RepoGroupAssignmentError
39 from rhodecode.lib.utils import jsonify, action_logger
39 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
40 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
40 from rhodecode.lib.auth import (
41 from rhodecode.lib.auth import (
41 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
42 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
@@ -181,7 +182,8 b' class UserGroupsController(BaseControlle'
181 h.flash(_('Error occurred during creation of user group %s') \
182 h.flash(_('Error occurred during creation of user group %s') \
182 % request.POST.get('users_group_name'), category='error')
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 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
188 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
187 def new(self):
189 def new(self):
@@ -467,5 +469,12 b' class UserGroupsController(BaseControlle'
467 c.group_members_obj = sorted((x.user for x in c.user_group.members),
469 c.group_members_obj = sorted((x.user for x in c.user_group.members),
468 key=lambda u: u.username.lower())
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 return render('admin/user_groups/user_group_edit.html')
480 return render('admin/user_groups/user_group_edit.html')
@@ -198,7 +198,9 b' class CompareController(BaseRepoControll'
198 c.statuses = c.rhodecode_db_repo.statuses(
198 c.statuses = c.rhodecode_db_repo.statuses(
199 [x.raw_id for x in c.commit_ranges])
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 return render('compare/compare_commits.html')
204 return render('compare/compare_commits.html')
203
205
204 if c.ancestor:
206 if c.ancestor:
@@ -24,16 +24,17 b' Home controller for RhodeCode Enterprise'
24
24
25 import logging
25 import logging
26 import time
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 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31 from sqlalchemy.sql import func
31 from sqlalchemy.sql import func
32
32
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasPermissionAllDecorator,
34 LoginRequired, HasPermissionAllDecorator, AuthUser,
35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
36 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
37 from rhodecode.lib.index import searcher_from_config
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.utils import jsonify
39 from rhodecode.lib.utils import jsonify
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.lib.utils2 import safe_unicode
@@ -134,7 +135,8 b' class HomeController(BaseController):'
134 'id': obj['name'],
135 'id': obj['name'],
135 'text': obj['name'],
136 'text': obj['name'],
136 'type': 'repo',
137 'type': 'repo',
137 'obj': obj['dbrepo']
138 'obj': obj['dbrepo'],
139 'url': url('summary_home', repo_name=obj['name'])
138 }
140 }
139 for obj in repo_iter]
141 for obj in repo_iter]
140
142
@@ -156,16 +158,45 b' class HomeController(BaseController):'
156 'id': obj.group_name,
158 'id': obj.group_name,
157 'text': obj.group_name,
159 'text': obj.group_name,
158 'type': 'group',
160 'type': 'group',
159 'obj': {}
161 'obj': {},
162 'url': url('repo_group_home', group_name=obj.group_name)
160 }
163 }
161 for obj in repo_groups_iter]
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 @LoginRequired()
194 @LoginRequired()
164 @XHRRequired()
195 @XHRRequired()
165 @jsonify
196 @jsonify
166 def repo_switcher_data(self):
197 def goto_switcher_data(self):
167 query = request.GET.get('query')
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 res = []
201 res = []
171 repo_groups = self._get_repo_group_list(query)
202 repo_groups = self._get_repo_group_list(query)
@@ -182,6 +213,19 b' class HomeController(BaseController):'
182 'children': repos
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 data = {
229 data = {
186 'more': False,
230 'more': False,
187 'results': res
231 'results': res
@@ -203,6 +247,7 b' class HomeController(BaseController):'
203 'text': _('Repositories'),
247 'text': _('Repositories'),
204 'children': repos
248 'children': repos
205 })
249 })
250
206 data = {
251 data = {
207 'more': False,
252 'more': False,
208 'results': res
253 'results': res
@@ -590,6 +590,8 b' class PullrequestsController(BaseRepoCon'
590 PullRequestModel().close_pull_request(
590 PullRequestModel().close_pull_request(
591 pull_request.pull_request_id, user)
591 pull_request.pull_request_id, user)
592 Session().commit()
592 Session().commit()
593 msg = _('Pull request was successfully merged and closed.')
594 h.flash(msg, category='success')
593 else:
595 else:
594 log.debug(
596 log.debug(
595 "The merge was not successful. Merge response: %s",
597 "The merge was not successful. Merge response: %s",
@@ -56,30 +56,33 b' class SearchController(BaseRepoControlle'
56 search_params = schema.deserialize(
56 search_params = schema.deserialize(
57 dict(search_query=request.GET.get('q'),
57 dict(search_query=request.GET.get('q'),
58 search_type=request.GET.get('type'),
58 search_type=request.GET.get('type'),
59 search_sort=request.GET.get('sort'),
59 page_limit=request.GET.get('page_limit'),
60 page_limit=request.GET.get('page_limit'),
60 requested_page=request.GET.get('page'))
61 requested_page=request.GET.get('page'))
61 )
62 )
62 except validation_schema.Invalid as e:
63 except validation_schema.Invalid as e:
63 errors = e.children
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 search_query = search_params.get('search_query')
71 search_query = search_params.get('search_query')
66 search_type = search_params.get('search_type')
72 search_type = search_params.get('search_type')
67
73 search_sort = search_params.get('search_sort')
68 if search_params.get('search_query'):
74 if search_params.get('search_query'):
69 page_limit = search_params['page_limit']
75 page_limit = search_params['page_limit']
70 requested_page = search_params['requested_page']
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 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
78 ip_addr=self.ip_addr)
80 ip_addr=self.ip_addr)
79
81
80 try:
82 try:
81 search_result = searcher.search(
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 formatted_results = Page(
87 formatted_results = Page(
85 search_result['results'], page=requested_page,
88 search_result['results'], page=requested_page,
@@ -97,6 +100,8 b' class SearchController(BaseRepoControlle'
97 errors = [
100 errors = [
98 validation_schema.Invalid(node, search_result['error'])]
101 validation_schema.Invalid(node, search_result['error'])]
99
102
103 c.sort = search_sort
104 c.url_generator = url_generator
100 c.errors = errors
105 c.errors = errors
101 c.formatted_results = formatted_results
106 c.formatted_results = formatted_results
102 c.runtime = execution_time
107 c.runtime = execution_time
@@ -299,6 +299,54 b' def _cached_perms_data(user_id, scope, u'
299 explicit, algo)
299 explicit, algo)
300 return permissions.calculate()
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 class PermissionCalculator(object):
351 class PermissionCalculator(object):
304
352
@@ -318,9 +366,9 b' class PermissionCalculator(object):'
318
366
319 self.default_user_id = User.get_default_user(cache=True).user_id
367 self.default_user_id = User.get_default_user(cache=True).user_id
320
368
321 self.permissions_repositories = {}
369 self.permissions_repositories = PermOriginDict()
322 self.permissions_repository_groups = {}
370 self.permissions_repository_groups = PermOriginDict()
323 self.permissions_user_groups = {}
371 self.permissions_user_groups = PermOriginDict()
324 self.permissions_global = set()
372 self.permissions_global = set()
325
373
326 self.default_repo_perms = Permission.get_default_repo_perms(
374 self.default_repo_perms = Permission.get_default_repo_perms(
@@ -355,19 +403,19 b' class PermissionCalculator(object):'
355 for perm in self.default_repo_perms:
403 for perm in self.default_repo_perms:
356 r_k = perm.UserRepoToPerm.repository.repo_name
404 r_k = perm.UserRepoToPerm.repository.repo_name
357 p = 'repository.admin'
405 p = 'repository.admin'
358 self.permissions_repositories[r_k] = p
406 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
359
407
360 # repository groups
408 # repository groups
361 for perm in self.default_repo_groups_perms:
409 for perm in self.default_repo_groups_perms:
362 rg_k = perm.UserRepoGroupToPerm.group.group_name
410 rg_k = perm.UserRepoGroupToPerm.group.group_name
363 p = 'group.admin'
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 # user groups
414 # user groups
367 for perm in self.default_user_group_perms:
415 for perm in self.default_user_group_perms:
368 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
416 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
369 p = 'usergroup.admin'
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 return self._permission_structure()
420 return self._permission_structure()
373
421
@@ -438,8 +486,7 b' class PermissionCalculator(object):'
438 self.permissions_global = self.permissions_global.difference(
486 self.permissions_global = self.permissions_global.difference(
439 _configurable)
487 _configurable)
440 for perm in perms:
488 for perm in perms:
441 self.permissions_global.add(
489 self.permissions_global.add(perm.permission.permission_name)
442 perm.permission.permission_name)
443
490
444 # user explicit global permissions
491 # user explicit global permissions
445 user_perms = Session().query(UserToPerm)\
492 user_perms = Session().query(UserToPerm)\
@@ -478,13 +525,16 b' class PermissionCalculator(object):'
478 # on given repo
525 # on given repo
479 for perm in self.default_repo_perms:
526 for perm in self.default_repo_perms:
480 r_k = perm.UserRepoToPerm.repository.repo_name
527 r_k = perm.UserRepoToPerm.repository.repo_name
528 o = PermOrigin.REPO_DEFAULT
481 if perm.Repository.private and not (
529 if perm.Repository.private and not (
482 perm.Repository.user_id == self.user_id):
530 perm.Repository.user_id == self.user_id):
483 # disable defaults for private repos,
531 # disable defaults for private repos,
484 p = 'repository.none'
532 p = 'repository.none'
533 o = PermOrigin.REPO_PRIVATE
485 elif perm.Repository.user_id == self.user_id:
534 elif perm.Repository.user_id == self.user_id:
486 # set admin if owner
535 # set admin if owner
487 p = 'repository.admin'
536 p = 'repository.admin'
537 o = PermOrigin.REPO_OWNER
488 else:
538 else:
489 p = perm.Permission.permission_name
539 p = perm.Permission.permission_name
490 # if we decide this user isn't inheriting permissions from
540 # if we decide this user isn't inheriting permissions from
@@ -492,15 +542,17 b' class PermissionCalculator(object):'
492 # permissions work
542 # permissions work
493 if not user_inherit_object_permissions:
543 if not user_inherit_object_permissions:
494 p = 'repository.none'
544 p = 'repository.none'
495 self.permissions_repositories[r_k] = p
545 self.permissions_repositories[r_k] = p, o
496
546
497 # defaults for repository groups taken from `default` user permission
547 # defaults for repository groups taken from `default` user permission
498 # on given group
548 # on given group
499 for perm in self.default_repo_groups_perms:
549 for perm in self.default_repo_groups_perms:
500 rg_k = perm.UserRepoGroupToPerm.group.group_name
550 rg_k = perm.UserRepoGroupToPerm.group.group_name
551 o = PermOrigin.REPOGROUP_DEFAULT
501 if perm.RepoGroup.user_id == self.user_id:
552 if perm.RepoGroup.user_id == self.user_id:
502 # set admin if owner
553 # set admin if owner
503 p = 'group.admin'
554 p = 'group.admin'
555 o = PermOrigin.REPOGROUP_OWNER
504 else:
556 else:
505 p = perm.Permission.permission_name
557 p = perm.Permission.permission_name
506
558
@@ -508,18 +560,19 b' class PermissionCalculator(object):'
508 # user we set him to .none so only explicit permissions work
560 # user we set him to .none so only explicit permissions work
509 if not user_inherit_object_permissions:
561 if not user_inherit_object_permissions:
510 p = 'group.none'
562 p = 'group.none'
511 self.permissions_repository_groups[rg_k] = p
563 self.permissions_repository_groups[rg_k] = p, o
512
564
513 # defaults for user groups taken from `default` user permission
565 # defaults for user groups taken from `default` user permission
514 # on given user group
566 # on given user group
515 for perm in self.default_user_group_perms:
567 for perm in self.default_user_group_perms:
516 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
568 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
517 p = perm.Permission.permission_name
569 p = perm.Permission.permission_name
570 o = PermOrigin.USERGROUP_DEFAULT
518 # if we decide this user isn't inheriting permissions from default
571 # if we decide this user isn't inheriting permissions from default
519 # user we set him to .none so only explicit permissions work
572 # user we set him to .none so only explicit permissions work
520 if not user_inherit_object_permissions:
573 if not user_inherit_object_permissions:
521 p = 'usergroup.none'
574 p = 'usergroup.none'
522 self.permissions_user_groups[u_k] = p
575 self.permissions_user_groups[u_k] = p, o
523
576
524 def _calculate_repository_permissions(self):
577 def _calculate_repository_permissions(self):
525 """
578 """
@@ -538,17 +591,20 b' class PermissionCalculator(object):'
538 multiple_counter = collections.defaultdict(int)
591 multiple_counter = collections.defaultdict(int)
539 for perm in user_repo_perms_from_user_group:
592 for perm in user_repo_perms_from_user_group:
540 r_k = perm.UserGroupRepoToPerm.repository.repo_name
593 r_k = perm.UserGroupRepoToPerm.repository.repo_name
594 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
541 multiple_counter[r_k] += 1
595 multiple_counter[r_k] += 1
542 p = perm.Permission.permission_name
596 p = perm.Permission.permission_name
597 o = PermOrigin.REPO_USERGROUP % ug_k
543
598
544 if perm.Repository.user_id == self.user_id:
599 if perm.Repository.user_id == self.user_id:
545 # set admin if owner
600 # set admin if owner
546 p = 'repository.admin'
601 p = 'repository.admin'
602 o = PermOrigin.REPO_OWNER
547 else:
603 else:
548 if multiple_counter[r_k] > 1:
604 if multiple_counter[r_k] > 1:
549 cur_perm = self.permissions_repositories[r_k]
605 cur_perm = self.permissions_repositories[r_k]
550 p = self._choose_permission(p, cur_perm)
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 # user explicit permissions for repositories, overrides any specified
609 # user explicit permissions for repositories, overrides any specified
554 # by the group permission
610 # by the group permission
@@ -556,16 +612,18 b' class PermissionCalculator(object):'
556 self.user_id, self.scope_repo_id)
612 self.user_id, self.scope_repo_id)
557 for perm in user_repo_perms:
613 for perm in user_repo_perms:
558 r_k = perm.UserRepoToPerm.repository.repo_name
614 r_k = perm.UserRepoToPerm.repository.repo_name
615 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
559 # set admin if owner
616 # set admin if owner
560 if perm.Repository.user_id == self.user_id:
617 if perm.Repository.user_id == self.user_id:
561 p = 'repository.admin'
618 p = 'repository.admin'
619 o = PermOrigin.REPO_OWNER
562 else:
620 else:
563 p = perm.Permission.permission_name
621 p = perm.Permission.permission_name
564 if not self.explicit:
622 if not self.explicit:
565 cur_perm = self.permissions_repositories.get(
623 cur_perm = self.permissions_repositories.get(
566 r_k, 'repository.none')
624 r_k, 'repository.none')
567 p = self._choose_permission(p, cur_perm)
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 def _calculate_repository_group_permissions(self):
628 def _calculate_repository_group_permissions(self):
571 """
629 """
@@ -583,32 +641,39 b' class PermissionCalculator(object):'
583 multiple_counter = collections.defaultdict(int)
641 multiple_counter = collections.defaultdict(int)
584 for perm in user_repo_group_perms_from_user_group:
642 for perm in user_repo_group_perms_from_user_group:
585 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
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 multiple_counter[g_k] += 1
646 multiple_counter[g_k] += 1
587 p = perm.Permission.permission_name
647 p = perm.Permission.permission_name
588 if perm.RepoGroup.user_id == self.user_id:
648 if perm.RepoGroup.user_id == self.user_id:
589 # set admin if owner
649 # set admin if owner
590 p = 'group.admin'
650 p = 'group.admin'
651 o = PermOrigin.REPOGROUP_OWNER
591 else:
652 else:
592 if multiple_counter[g_k] > 1:
653 if multiple_counter[g_k] > 1:
593 cur_perm = self.permissions_repository_groups[g_k]
654 cur_perm = self.permissions_repository_groups[g_k]
594 p = self._choose_permission(p, cur_perm)
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 # user explicit permissions for repository groups
658 # user explicit permissions for repository groups
598 user_repo_groups_perms = Permission.get_default_group_perms(
659 user_repo_groups_perms = Permission.get_default_group_perms(
599 self.user_id, self.scope_repo_group_id)
660 self.user_id, self.scope_repo_group_id)
600 for perm in user_repo_groups_perms:
661 for perm in user_repo_groups_perms:
601 rg_k = perm.UserRepoGroupToPerm.group.group_name
662 rg_k = perm.UserRepoGroupToPerm.group.group_name
663 u_k = perm.UserRepoGroupToPerm.user.username
664 o = PermOrigin.REPOGROUP_USER % u_k
665
602 if perm.RepoGroup.user_id == self.user_id:
666 if perm.RepoGroup.user_id == self.user_id:
603 # set admin if owner
667 # set admin if owner
604 p = 'group.admin'
668 p = 'group.admin'
669 o = PermOrigin.REPOGROUP_OWNER
605 else:
670 else:
606 p = perm.Permission.permission_name
671 p = perm.Permission.permission_name
607 if not self.explicit:
672 if not self.explicit:
608 cur_perm = self.permissions_repository_groups.get(
673 cur_perm = self.permissions_repository_groups.get(
609 rg_k, 'group.none')
674 rg_k, 'group.none')
610 p = self._choose_permission(p, cur_perm)
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 def _calculate_user_group_permissions(self):
678 def _calculate_user_group_permissions(self):
614 """
679 """
@@ -623,24 +688,29 b' class PermissionCalculator(object):'
623 for perm in user_group_from_user_group:
688 for perm in user_group_from_user_group:
624 g_k = perm.UserGroupUserGroupToPerm\
689 g_k = perm.UserGroupUserGroupToPerm\
625 .target_user_group.users_group_name
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 multiple_counter[g_k] += 1
694 multiple_counter[g_k] += 1
627 p = perm.Permission.permission_name
695 p = perm.Permission.permission_name
628 if multiple_counter[g_k] > 1:
696 if multiple_counter[g_k] > 1:
629 cur_perm = self.permissions_user_groups[g_k]
697 cur_perm = self.permissions_user_groups[g_k]
630 p = self._choose_permission(p, cur_perm)
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 # user explicit permission for user groups
701 # user explicit permission for user groups
634 user_user_groups_perms = Permission.get_default_user_group_perms(
702 user_user_groups_perms = Permission.get_default_user_group_perms(
635 self.user_id, self.scope_user_group_id)
703 self.user_id, self.scope_user_group_id)
636 for perm in user_user_groups_perms:
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 p = perm.Permission.permission_name
707 p = perm.Permission.permission_name
708 o = PermOrigin.USERGROUP_USER % u_k
639 if not self.explicit:
709 if not self.explicit:
640 cur_perm = self.permissions_user_groups.get(
710 cur_perm = self.permissions_user_groups.get(
641 u_k, 'usergroup.none')
711 ug_k, 'usergroup.none')
642 p = self._choose_permission(p, cur_perm)
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 def _choose_permission(self, new_perm, cur_perm):
715 def _choose_permission(self, new_perm, cur_perm):
646 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
716 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
@@ -865,6 +935,10 b' class AuthUser(object):'
865 return auth_tokens
935 return auth_tokens
866
936
867 @property
937 @property
938 def is_default(self):
939 return self.username == User.DEFAULT_USER
940
941 @property
868 def is_admin(self):
942 def is_admin(self):
869 return self.admin
943 return self.admin
870
944
@@ -1095,6 +1169,7 b' class LoginRequired(object):'
1095 return get_cython_compat_decorator(self.__wrapper, func)
1169 return get_cython_compat_decorator(self.__wrapper, func)
1096
1170
1097 def __wrapper(self, func, *fargs, **fkwargs):
1171 def __wrapper(self, func, *fargs, **fkwargs):
1172 from rhodecode.lib import helpers as h
1098 cls = fargs[0]
1173 cls = fargs[0]
1099 user = cls._rhodecode_user
1174 user = cls._rhodecode_user
1100 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1175 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
@@ -1102,7 +1177,6 b' class LoginRequired(object):'
1102 # check if our IP is allowed
1177 # check if our IP is allowed
1103 ip_access_valid = True
1178 ip_access_valid = True
1104 if not user.ip_allowed:
1179 if not user.ip_allowed:
1105 from rhodecode.lib import helpers as h
1106 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1180 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1107 category='warning')
1181 category='warning')
1108 ip_access_valid = False
1182 ip_access_valid = False
@@ -1154,7 +1228,7 b' class LoginRequired(object):'
1154
1228
1155 log.debug('redirecting to login page with %s' % (came_from,))
1229 log.debug('redirecting to login page with %s' % (came_from,))
1156 return redirect(
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 class NotAnonymous(object):
1234 class NotAnonymous(object):
@@ -1180,7 +1254,8 b' class NotAnonymous(object):'
1180 h.flash(_('You need to be a registered user to '
1254 h.flash(_('You need to be a registered user to '
1181 'perform this action'),
1255 'perform this action'),
1182 category='warning')
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 else:
1259 else:
1185 return func(*fargs, **fkwargs)
1260 return func(*fargs, **fkwargs)
1186
1261
@@ -1263,7 +1338,8 b' class PermsDecorator(object):'
1263 import rhodecode.lib.helpers as h
1338 import rhodecode.lib.helpers as h
1264 h.flash(_('You need to be signed in to view this page'),
1339 h.flash(_('You need to be signed in to view this page'),
1265 category='warning')
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 else:
1344 else:
1269 # redirect with forbidden ret code
1345 # redirect with forbidden ret code
@@ -35,7 +35,7 b' def makedate():'
35 return time.mktime(lt), tz
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 Makes a local datetime object out of unix timestamp
40 Makes a local datetime object out of unix timestamp
41
41
@@ -43,7 +43,7 b' def date_fromtimestamp(unixts, tzoffset='
43 :param tzoffset:
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 def date_astimestamp(value):
49 def date_astimestamp(value):
@@ -537,7 +537,6 b' class DbManage(object):'
537 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
537 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
538 ('support_url', '', 'unicode'),
538 ('support_url', '', 'unicode'),
539 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
539 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
540 ('license_key', '', 'unicode'),
541 ('show_revision_number', True, 'bool'),
540 ('show_revision_number', True, 'bool'),
542 ('show_sha_length', 12, 'int'),
541 ('show_sha_length', 12, 'int'),
543 ]
542 ]
@@ -36,11 +36,14 b' import urlparse'
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 import pygments
39
40
40 from datetime import datetime
41 from datetime import datetime
41 from functools import partial
42 from functools import partial
42 from pygments.formatters.html import HtmlFormatter
43 from pygments.formatters.html import HtmlFormatter
43 from pygments import highlight as code_highlight
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 from pylons import url
47 from pylons import url
45 from pylons.i18n.translation import _, ungettext
48 from pylons.i18n.translation import _, ungettext
46 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
@@ -68,8 +71,8 b' from rhodecode.lib.annotate import annot'
68 from rhodecode.lib.action_parser import action_parser
71 from rhodecode.lib.action_parser import action_parser
69 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
70 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
71 get_commit_safe, datetime_to_time, time_to_datetime, AttributeDict, \
74 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
72 safe_int, md5, md5_safe
75 AttributeDict, safe_int, md5, md5_safe
73 from rhodecode.lib.markup_renderer import MarkupRenderer
76 from rhodecode.lib.markup_renderer import MarkupRenderer
74 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
75 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
@@ -307,6 +310,176 b' class CodeHtmlFormatter(HtmlFormatter):'
307 yield 0, '</td></tr></table>'
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 def pygmentize(filenode, **kwargs):
483 def pygmentize(filenode, **kwargs):
311 """
484 """
312 pygmentize function using pygments
485 pygmentize function using pygments
@@ -476,13 +649,20 b' short_id = lambda x: x[:12]'
476 hide_credentials = lambda x: ''.join(credentials_filter(x))
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 title = value or format_date(datetime_iso)
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 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
656 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
484 tzinfo = '+00:00'
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 return literal(
666 return literal(
487 '<time class="timeago tooltip" '
667 '<time class="timeago tooltip" '
488 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
668 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
@@ -42,7 +42,6 b' class BaseSearch(object):'
42 def search(self, query, document_type, search_user, repo_name=None):
42 def search(self, query, document_type, search_user, repo_name=None):
43 raise Exception('NotImplemented')
43 raise Exception('NotImplemented')
44
44
45
46 def searcher_from_config(config, prefix='search.'):
45 def searcher_from_config(config, prefix='search.'):
47 _config = {}
46 _config = {}
48 for key in config.keys():
47 for key in config.keys():
@@ -25,6 +25,7 b' Index schema for RhodeCode'
25 from __future__ import absolute_import
25 from __future__ import absolute_import
26 import logging
26 import logging
27 import os
27 import os
28 import re
28
29
29 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
30
31
@@ -59,6 +60,7 b' FRAGMENTER = ContextFragmenter(200)'
59 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
60
61
61
62
63
62 class Search(BaseSearch):
64 class Search(BaseSearch):
63
65
64 name = 'whoosh'
66 name = 'whoosh'
@@ -90,7 +92,19 b' class Search(BaseSearch):'
90 if self.searcher:
92 if self.searcher:
91 self.searcher.close()
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 log.debug(u'QUERY: %s on %s', query, document_type)
108 log.debug(u'QUERY: %s on %s', query, document_type)
95 result = {
109 result = {
96 'results': [],
110 'results': [],
@@ -109,13 +123,18 b' class Search(BaseSearch):'
109 query = qp.parse(unicode(query))
123 query = qp.parse(unicode(query))
110 log.debug('query: %s (%s)' % (query, repr(query)))
124 log.debug('query: %s (%s)' % (query, repr(query)))
111
125
112 sortedby = None
126 reverse, sortedby = False, None
113 if search_type == 'message':
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 whoosh_results = self.searcher.search(
135 whoosh_results = self.searcher.search(
117 query, filter=allowed_repos_filter, limit=None,
136 query, filter=allowed_repos_filter, limit=None,
118 sortedby=sortedby,)
137 sortedby=sortedby, reverse=reverse)
119
138
120 # fixes for 32k limit that whoosh uses for highlight
139 # fixes for 32k limit that whoosh uses for highlight
121 whoosh_results.fragmenter.charlimit = None
140 whoosh_results.fragmenter.charlimit = None
@@ -63,7 +63,7 b' COMMIT_SCHEMA = Schema('
63 repository_id=NUMERIC(unique=True, stored=True),
63 repository_id=NUMERIC(unique=True, stored=True),
64 commit_idx=NUMERIC(stored=True, sortable=True),
64 commit_idx=NUMERIC(stored=True, sortable=True),
65 commit_idx_sort=ID(),
65 commit_idx_sort=ID(),
66 date=NUMERIC(stored=True),
66 date=NUMERIC(stored=True, sortable=True),
67 owner=TEXT(stored=True),
67 owner=TEXT(stored=True),
68 author=TEXT(stored=True),
68 author=TEXT(stored=True),
69 message=FieldType(format=Characters(), analyzer=ANALYZER,
69 message=FieldType(format=Characters(), analyzer=ANALYZER,
@@ -755,8 +755,8 b' def create_test_env(repos_test_path, con'
755 # PART TWO make test repo
755 # PART TWO make test repo
756 log.debug('making test vcs repositories')
756 log.debug('making test vcs repositories')
757
757
758 idx_path = config['app_conf']['search.location']
758 idx_path = config['search.location']
759 data_path = config['app_conf']['cache_dir']
759 data_path = config['cache_dir']
760
760
761 #clean index and data
761 # clean index and data
762 if idx_path and os.path.exists(idx_path):
762 if idx_path and os.path.exists(idx_path):
@@ -787,7 +787,6 b' def create_test_env(repos_test_path, con'
787 tar.extractall(jn(TESTS_TMP_PATH, SVN_REPO))
787 tar.extractall(jn(TESTS_TMP_PATH, SVN_REPO))
788
788
789
789
790
791 #==============================================================================
790 #==============================================================================
792 # PASTER COMMANDS
791 # PASTER COMMANDS
793 #==============================================================================
792 #==============================================================================
@@ -608,6 +608,16 b' def time_to_datetime(tm):'
608 return datetime.datetime.fromtimestamp(tm)
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 MENTIONS_REGEX = re.compile(
621 MENTIONS_REGEX = re.compile(
612 # ^@ or @ without any special chars in front
622 # ^@ or @ without any special chars in front
613 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
623 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
@@ -409,7 +409,9 b' class BaseRepository(object):'
409 shadow_repository_path, target_ref, source_repo,
409 shadow_repository_path, target_ref, source_repo,
410 source_ref, message, user_name, user_email, dry_run=dry_run)
410 source_ref, message, user_name, user_email, dry_run=dry_run)
411 except RepositoryError:
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 return MergeResponse(
415 return MergeResponse(
414 False, False, None, MergeFailureReason.UNKNOWN)
416 False, False, None, MergeFailureReason.UNKNOWN)
415
417
@@ -30,7 +30,7 b' from StringIO import StringIO'
30
30
31 from zope.cachedescriptors.property import Lazy as LazyProperty
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 from rhodecode.lib.utils import safe_unicode, safe_str
34 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.utils2 import safe_int
36 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.conf import settings
@@ -95,7 +95,7 b' class GitCommit(base.BaseCommit):'
95 if value:
95 if value:
96 value = safe_unicode(value)
96 value = safe_unicode(value)
97 elif attr == "date":
97 elif attr == "date":
98 value = date_fromtimestamp(*value)
98 value = utcdate_fromtimestamp(*value)
99 elif attr == "parents":
99 elif attr == "parents":
100 value = self._make_commits(value)
100 value = self._make_commits(value)
101 self.__dict__[attr] = value
101 self.__dict__[attr] = value
@@ -135,7 +135,7 b' class GitCommit(base.BaseCommit):'
135 def date(self):
135 def date(self):
136 unix_ts, tz = self._remote.get_object_attrs(
136 unix_ts, tz = self._remote.get_object_attrs(
137 self.raw_id, self._date_property, self._date_tz_property)
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 @LazyProperty
140 @LazyProperty
141 def status(self):
141 def status(self):
@@ -31,7 +31,7 b' import time'
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 from rhodecode.lib.compat import OrderedDict
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 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils import safe_unicode, safe_str
36 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs import connection, path as vcspath
37 from rhodecode.lib.vcs.backends.base import (
37 from rhodecode.lib.vcs.backends.base import (
@@ -269,7 +269,7 b' class GitRepository(BaseRepository):'
269 Returns last change made on this repository as
269 Returns last change made on this repository as
270 `datetime.datetime` object.
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 def _get_mtime(self):
274 def _get_mtime(self):
275 try:
275 try:
@@ -853,7 +853,8 b' class GitRepository(BaseRepository):'
853 shadow_repo._checkout(pr_branch, create=True)
853 shadow_repo._checkout(pr_branch, create=True)
854 try:
854 try:
855 shadow_repo._local_fetch(source_repo.path, source_ref.name)
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 return MergeResponse(
858 return MergeResponse(
858 False, False, None, MergeFailureReason.MISSING_COMMIT)
859 False, False, None, MergeFailureReason.MISSING_COMMIT)
859
860
@@ -863,7 +864,8 b' class GitRepository(BaseRepository):'
863 shadow_repo._local_merge(merge_message, merger_name, merger_email,
864 shadow_repo._local_merge(merge_message, merger_name, merger_email,
864 [source_ref.commit_id])
865 [source_ref.commit_id])
865 merge_possible = True
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 merge_possible = False
869 merge_possible = False
868 merge_failure_reason = MergeFailureReason.MERGE_FAILED
870 merge_failure_reason = MergeFailureReason.MERGE_FAILED
869
871
@@ -877,7 +879,9 b' class GitRepository(BaseRepository):'
877 # cannot retrieve the merge commit.
879 # cannot retrieve the merge commit.
878 shadow_repo = GitRepository(shadow_repository_path)
880 shadow_repo = GitRepository(shadow_repository_path)
879 merge_commit_id = shadow_repo.branches[pr_branch]
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 merge_succeeded = False
885 merge_succeeded = False
882 merge_failure_reason = MergeFailureReason.PUSH_FAILED
886 merge_failure_reason = MergeFailureReason.PUSH_FAILED
883 else:
887 else:
@@ -26,7 +26,7 b' import os'
26
26
27 from zope.cachedescriptors.property import Lazy as LazyProperty
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 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.utils import safe_str, safe_unicode
31 from rhodecode.lib.vcs import path as vcspath
31 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
@@ -78,7 +78,7 b' class MercurialCommit(base.BaseCommit):'
78 elif attr == "affected_files":
78 elif attr == "affected_files":
79 value = map(safe_unicode, value)
79 value = map(safe_unicode, value)
80 elif attr == "date":
80 elif attr == "date":
81 value = date_fromtimestamp(*value)
81 value = utcdate_fromtimestamp(*value)
82 elif attr in ["children", "parents"]:
82 elif attr in ["children", "parents"]:
83 value = self._make_commits(value)
83 value = self._make_commits(value)
84 self.__dict__[attr] = value
84 self.__dict__[attr] = value
@@ -114,7 +114,7 b' class MercurialCommit(base.BaseCommit):'
114
114
115 @LazyProperty
115 @LazyProperty
116 def date(self):
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 @LazyProperty
119 @LazyProperty
120 def status(self):
120 def status(self):
@@ -22,6 +22,7 b''
22 HG repository module
22 HG repository module
23 """
23 """
24
24
25 import logging
25 import binascii
26 import binascii
26 import os
27 import os
27 import re
28 import re
@@ -31,9 +32,8 b' import urllib'
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 from zope.cachedescriptors.property import Lazy as LazyProperty
32
33
33 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib.datelib import (
35 from rhodecode.lib.datelib import (date_to_timestamp_plus_offset,
35 date_fromtimestamp, makedate, date_to_timestamp_plus_offset,
36 utcdate_fromtimestamp, makedate, date_astimestamp)
36 date_astimestamp)
37 from rhodecode.lib.utils import safe_unicode, safe_str
37 from rhodecode.lib.utils import safe_unicode, safe_str
38 from rhodecode.lib.vcs import connection
38 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs.backends.base import (
39 from rhodecode.lib.vcs.backends.base import (
@@ -50,6 +50,8 b' from rhodecode.lib.vcs.exceptions import'
50 hexlify = binascii.hexlify
50 hexlify = binascii.hexlify
51 nullid = "\0" * 20
51 nullid = "\0" * 20
52
52
53 log = logging.getLogger(__name__)
54
53
55
54 class MercurialRepository(BaseRepository):
56 class MercurialRepository(BaseRepository):
55 """
57 """
@@ -365,7 +367,7 b' class MercurialRepository(BaseRepository'
365 Returns last change made on this repository as
367 Returns last change made on this repository as
366 `datetime.datetime` object
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 def _get_mtime(self):
372 def _get_mtime(self):
371 try:
373 try:
@@ -605,6 +607,10 b' class MercurialRepository(BaseRepository'
605 self._update(bookmark_name)
607 self._update(bookmark_name)
606 return self._identify(), True
608 return self._identify(), True
607 except RepositoryError:
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 # Cleanup any rebase leftovers
614 # Cleanup any rebase leftovers
609 self._remote.rebase(abort=True)
615 self._remote.rebase(abort=True)
610 self._remote.update(clean=True)
616 self._remote.update(clean=True)
@@ -642,6 +648,8 b' class MercurialRepository(BaseRepository'
642 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
648 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
643 if not os.path.exists(shadow_repository_path):
649 if not os.path.exists(shadow_repository_path):
644 self._local_clone(shadow_repository_path)
650 self._local_clone(shadow_repository_path)
651 log.debug(
652 'Prepared shadow repository in %s', shadow_repository_path)
645
653
646 return shadow_repository_path
654 return shadow_repository_path
647
655
@@ -664,12 +672,15 b' class MercurialRepository(BaseRepository'
664
672
665 shadow_repo = self._get_shadow_instance(shadow_repository_path)
673 shadow_repo = self._get_shadow_instance(shadow_repository_path)
666
674
675 log.debug('Pulling in target reference %s', target_ref)
667 self._validate_pull_reference(target_ref)
676 self._validate_pull_reference(target_ref)
668 shadow_repo._local_pull(self.path, target_ref)
677 shadow_repo._local_pull(self.path, target_ref)
669 try:
678 try:
679 log.debug('Pulling in source reference %s', source_ref)
670 source_repo._validate_pull_reference(source_ref)
680 source_repo._validate_pull_reference(source_ref)
671 shadow_repo._local_pull(source_repo.path, source_ref)
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 return MergeResponse(
684 return MergeResponse(
674 False, False, None, MergeFailureReason.MISSING_COMMIT)
685 False, False, None, MergeFailureReason.MISSING_COMMIT)
675
686
@@ -681,7 +692,8 b' class MercurialRepository(BaseRepository'
681 target_ref, merge_message, merger_name, merger_email,
692 target_ref, merge_message, merger_name, merger_email,
682 source_ref)
693 source_ref)
683 merge_possible = True
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 merge_possible = False
697 merge_possible = False
686 merge_failure_reason = MergeFailureReason.MERGE_FAILED
698 merge_failure_reason = MergeFailureReason.MERGE_FAILED
687
699
@@ -706,6 +718,9 b' class MercurialRepository(BaseRepository'
706 enable_hooks=True)
718 enable_hooks=True)
707 merge_succeeded = True
719 merge_succeeded = True
708 except RepositoryError:
720 except RepositoryError:
721 log.exception(
722 'Failure when doing local push from the shadow '
723 'repository to the target repository.')
709 merge_succeeded = False
724 merge_succeeded = False
710 merge_failure_reason = MergeFailureReason.PUSH_FAILED
725 merge_failure_reason = MergeFailureReason.PUSH_FAILED
711 else:
726 else:
@@ -1593,7 +1593,7 b' class Repository(Base, BaseModel):'
1593 'repo_id': repo.repo_id,
1593 'repo_id': repo.repo_id,
1594 'repo_name': repo.repo_name,
1594 'repo_name': repo.repo_name,
1595 'repo_type': repo.repo_type,
1595 'repo_type': repo.repo_type,
1596 'clone_uri': repo.clone_uri,
1596 'clone_uri': repo.clone_uri or '',
1597 'private': repo.private,
1597 'private': repo.private,
1598 'created_on': repo.created_on,
1598 'created_on': repo.created_on,
1599 'description': repo.description,
1599 'description': repo.description,
@@ -2794,7 +2794,9 b' class CacheKey(Base, BaseModel):'
2794
2794
2795 Session().commit()
2795 Session().commit()
2796 except Exception:
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 Session().rollback()
2800 Session().rollback()
2799
2801
2800 @classmethod
2802 @classmethod
@@ -396,10 +396,15 b' class PullRequestModel(BaseModel):'
396 return commit_ids
396 return commit_ids
397
397
398 def merge(self, pull_request, user, extras):
398 def merge(self, pull_request, user, extras):
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
399 merge_state = self._merge_pull_request(pull_request, user, extras)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
400 if merge_state.executed:
401 if merge_state.executed:
402 log.debug(
403 "Merge was successful, updating the pull request comments.")
401 self._comment_and_close_pr(pull_request, user, merge_state)
404 self._comment_and_close_pr(pull_request, user, merge_state)
402 self._log_action('user_merged_pull_request', user, pull_request)
405 self._log_action('user_merged_pull_request', user, pull_request)
406 else:
407 log.warn("Merge failed, not updating the pull request.")
403 return merge_state
408 return merge_state
404
409
405 def _merge_pull_request(self, pull_request, user, extras):
410 def _merge_pull_request(self, pull_request, user, extras):
@@ -907,15 +912,20 b' class PullRequestModel(BaseModel):'
907 """
912 """
908 Try to merge the pull request and return the merge status.
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 target_vcs = pull_request.target_repo.scm_instance()
918 target_vcs = pull_request.target_repo.scm_instance()
911 target_ref = self._refresh_reference(
919 target_ref = self._refresh_reference(
912 pull_request.target_ref_parts, target_vcs)
920 pull_request.target_ref_parts, target_vcs)
913
921
914 target_locked = pull_request.target_repo.locked
922 target_locked = pull_request.target_repo.locked
915 if target_locked and target_locked[0]:
923 if target_locked and target_locked[0]:
924 log.debug("The target repository is locked.")
916 merge_state = MergeResponse(
925 merge_state = MergeResponse(
917 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
926 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
918 elif self._needs_merge_state_refresh(pull_request, target_ref):
927 elif self._needs_merge_state_refresh(pull_request, target_ref):
928 log.debug("Refreshing the merge status of the repository.")
919 merge_state = self._refresh_merge_state(
929 merge_state = self._refresh_merge_state(
920 pull_request, target_vcs, target_ref)
930 pull_request, target_vcs, target_ref)
921 else:
931 else:
@@ -923,6 +933,7 b' class PullRequestModel(BaseModel):'
923 _last_merge_status == MergeFailureReason.NONE
933 _last_merge_status == MergeFailureReason.NONE
924 merge_state = MergeResponse(
934 merge_state = MergeResponse(
925 possible, False, None, pull_request._last_merge_status)
935 possible, False, None, pull_request._last_merge_status)
936 log.debug("Merge response: %s", merge_state)
926 return merge_state
937 return merge_state
927
938
928 def _refresh_reference(self, reference, vcs_repository):
939 def _refresh_reference(self, reference, vcs_repository):
@@ -449,7 +449,7 b' class ScmModel(BaseModel):'
449 return tip
449 return tip
450
450
451 def _sanitize_path(self, f_path):
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 raise NonRelativePathError('%s is not an relative path' % f_path)
453 raise NonRelativePathError('%s is not an relative path' % f_path)
454 if f_path:
454 if f_path:
455 f_path = os.path.normpath(f_path)
455 f_path = os.path.normpath(f_path)
@@ -493,7 +493,7 b' class UserModel(BaseModel):'
493 log.error(traceback.format_exc())
493 log.error(traceback.format_exc())
494 raise
494 raise
495
495
496 def reset_password_link(self, data):
496 def reset_password_link(self, data, pwd_reset_url):
497 from rhodecode.lib.celerylib import tasks, run_task
497 from rhodecode.lib.celerylib import tasks, run_task
498 from rhodecode.model.notification import EmailNotificationModel
498 from rhodecode.model.notification import EmailNotificationModel
499 user_email = data['email']
499 user_email = data['email']
@@ -502,12 +502,8 b' class UserModel(BaseModel):'
502 if user:
502 if user:
503 log.debug('password reset user found %s', user)
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 email_kwargs = {
505 email_kwargs = {
510 'password_reset_url': password_reset_url,
506 'password_reset_url': pwd_reset_url,
511 'user': user,
507 'user': user,
512 'email': user_email,
508 'email': user_email,
513 'date': datetime.datetime.now()
509 'date': datetime.datetime.now()
@@ -216,7 +216,13 b' class UserGroupModel(BaseModel):'
216 if 'user' in form_data:
216 if 'user' in form_data:
217 owner = form_data['user']
217 owner = form_data['user']
218 if isinstance(owner, basestring):
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 if 'users_group_members' in form_data:
227 if 'users_group_members' in form_data:
222 members_id_list = self._clean_members_data(
228 members_id_list = self._clean_members_data(
@@ -51,6 +51,11 b' class SearchParamsSchema(colander.Mappin'
51 colander.String(),
51 colander.String(),
52 missing='content',
52 missing='content',
53 validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
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 page_limit = colander.SchemaNode(
59 page_limit = colander.SchemaNode(
55 colander.Integer(),
60 colander.Integer(),
56 missing=10,
61 missing=10,
@@ -38,9 +38,11 b' from sqlalchemy.sql.expression import tr'
38 from sqlalchemy.util import OrderedSet
38 from sqlalchemy.util import OrderedSet
39 from webhelpers.pylonslib.secure_form import authentication_token
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 from rhodecode.config.routing import ADMIN_PREFIX
44 from rhodecode.config.routing import ADMIN_PREFIX
42 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
45 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
43 from rhodecode.lib.exceptions import LdapImportError
44 from rhodecode.lib.utils import repo_name_slug, make_db_config
46 from rhodecode.lib.utils import repo_name_slug, make_db_config
45 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
47 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
46 from rhodecode.lib.vcs.backends.git.repository import GitRepository
48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
@@ -437,8 +439,7 b' def ValidAuth():'
437 password = value['password']
439 password = value['password']
438 username = value['username']
440 username = value['username']
439
441
440 if not authenticate(username, password, '',
442 if not authenticate(username, password, '', HTTP_TYPE,
441 HTTP_TYPE,
442 skip_missing=True):
443 skip_missing=True):
443 user = User.get_by_username(username)
444 user = User.get_by_username(username)
444 if user and not user.active:
445 if user and not user.active:
@@ -448,7 +449,7 b' def ValidAuth():'
448 msg, value, state, error_dict={'username': msg}
449 msg, value, state, error_dict={'username': msg}
449 )
450 )
450 else:
451 else:
451 log.warning('user %s failed to authenticate', username)
452 log.warning('user `%s` failed to authenticate', username)
452 msg = M(self, 'invalid_username', state)
453 msg = M(self, 'invalid_username', state)
453 msg2 = M(self, 'invalid_password', state)
454 msg2 = M(self, 'invalid_password', state)
454 raise formencode.Invalid(
455 raise formencode.Invalid(
@@ -986,28 +987,71 b' def ValidAuthPlugins():'
986 'import_duplicate': _(
987 'import_duplicate': _(
987 u'Plugins %(loaded)s and %(next_to_load)s '
988 u'Plugins %(loaded)s and %(next_to_load)s '
988 u'both export the same name'),
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 def _to_python(self, value, state):
999 def _to_python(self, value, state):
992 # filter empty values
1000 # filter empty values
993 return filter(lambda s: s not in [None, ''], value)
1001 return filter(lambda s: s not in [None, ''], value)
994
1002
995 def validate_python(self, value, state):
1003 def _validate_legacy_plugin_id(self, plugin_id, value, state):
996 from rhodecode.authentication.base import loadplugin
1004 """
997 module_list = value
1005 Validates that the plugin import works. It also checks that the
998 unique_names = {}
1006 plugin has an includeme attribute.
1007 """
999 try:
1008 try:
1000 for module in module_list:
1009 plugin = _import_legacy_plugin(plugin_id)
1001 plugin = loadplugin(module)
1010 except Exception as e:
1002 plugin_name = plugin.name
1011 log.exception(
1003 if plugin_name in unique_names:
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:
1004 msg = M(self, 'import_duplicate', state,
1050 msg = M(self, 'import_duplicate', state,
1005 loaded=unique_names[plugin_name],
1051 loaded=unique_names[plugin.name],
1006 next_to_load=plugin_name)
1052 next_to_load=plugin)
1007 raise formencode.Invalid(msg, value, state)
1053 raise formencode.Invalid(msg, value, state)
1008 unique_names[plugin_name] = plugin
1054 unique_names[plugin.name] = plugin
1009 except (KeyError, AttributeError, TypeError) as e:
1010 raise formencode.Invalid(str(e), value, state)
1011
1055
1012 return _validator
1056 return _validator
1013
1057
@@ -514,6 +514,26 b' div.search-code-body {'
514 .match { background-color: #faffa6;}
514 .match { background-color: #faffa6;}
515 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
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 div.annotatediv { margin-left: 2px; margin-right: 4px; }
539 div.annotatediv { margin-left: 2px; margin-right: 4px; }
@@ -353,7 +353,12 b''
353 .middle-group{
353 .middle-group{
354 width: 10%;
354 width: 10%;
355 text-align: center;
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 .reviewer {
1234 .reviewer {
1235 float: left;
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 .reviewer_member_remove {
1246 .reviewer_member_remove {
@@ -80,6 +80,11 b''
80 [tag="recommends"] { &:extend(.tag7); }
80 [tag="recommends"] { &:extend(.tag7); }
81 [tag="see"] { &:extend(.tag8); }
81 [tag="see"] { &:extend(.tag8); }
82
82
83 .perm_overriden {
84 text-decoration: line-through;
85 opacity: 0.6;
86 }
87
83 .perm_tag {
88 .perm_tag {
84 &:extend(.tag);
89 &:extend(.tag);
85
90
@@ -1,45 +1,50 b''
1 /* This file is automatically generated. DO NOT change it manually.
1
2 * If this file needs to be modified, edit
2 /******************************************************************************
3 * rhodecode/utils/file_generation/js_routes_data.py
3 * *
4 * and run the script invoke -r scripts/ generate.js-routes .
4 * DO NOT CHANGE THIS FILE MANUALLY *
5 */
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 function registerRCRoutes() {
12 function registerRCRoutes() {
7 // routes registration
13 // routes registration
8 pyroutes.register('home', '/', []);
14 pyroutes.register('home', '/', []);
9 pyroutes.register('new_gist', '/_admin/gists/new', []);
15 pyroutes.register('user_autocomplete_data', '/_users', []);
10 pyroutes.register('gists', '/_admin/gists', []);
11 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 pyroutes.register('new_repo', '/_admin/create_repository', []);
12 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
13 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
18 pyroutes.register('gists', '/_admin/gists', []);
14 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
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 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
25 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
16 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
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 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
27 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
24 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
28 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
25 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
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']);
30 pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']);
27 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
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 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
45 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
29 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
30 pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
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']);
48 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
32 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
49 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
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']);
43 }
50 }
44
45 registerRCRoutes(); No newline at end of file
@@ -190,7 +190,7 b' var AgeModule = (function () {'
190
190
191 },
191 },
192 createTimeComponent: function(dateTime, text) {
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 if (reviewer){
30 if (reviewer){
31 // mark as to-remove
31 // mark as to-remove
32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
33 obj.css("text-decoration", "line-through");
34 obj.addClass('to-delete');
33 obj.addClass('to-delete');
35 // now delete the input
34 // now delete the input
36 $('#reviewer_{0}_input'.format(reviewer_id)).remove();
35 $('#reviewer_{0}_input'.format(reviewer_id)).remove();
@@ -20,9 +20,11 b''
20
20
21
21
22 import pylons
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 def add_renderer_globals(event):
30 def add_renderer_globals(event):
@@ -33,8 +35,11 b' def add_renderer_globals(event):'
33 event['c'] = pylons.tmpl_context
35 event['c'] = pylons.tmpl_context
34 event['url'] = pylons.url
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 # Add Pyramid translation as '_' to context
42 # Add Pyramid translation as '_' to context
37 request = event['request']
38 event['_'] = request.translate
43 event['_'] = request.translate
39 event['localizer'] = request.localizer
44 event['localizer'] = request.localizer
40
45
@@ -49,46 +49,44 b''
49 <div class="fields">
49 <div class="fields">
50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'))}
50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'))}
51 <div class="form">
51 <div class="form">
52
52 %for node in plugin.get_settings_schema():
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 <div class="field">
55 <div class="field">
55 <div class="label ${label_cls}"><label for="${node.name}">${node.title}</label></div>
56 <div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div>
56 %if node.widget in ["string", "int", "unicode"]:
57 <div class="input">
57 <div class="input">
58 ${h.text(node.name, class_="medium")}
58 %if node.widget in ["string", "int", "unicode"]:
59 <p class="help-block">${node.description}</p>
59 ${h.text(node.name, defaults.get(node.name), class_="medium")}
60 </div>
61 %elif node.widget == "password":
60 %elif node.widget == "password":
62 <div class="input">
61 ${h.password(node.name, defaults.get(node.name), class_="medium")}
63 ${h.password(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":
67 ${node.default}
68 %else:
69 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
70 %endif
71 %if node.name in errors:
72 <span class="error-message">${errors.get(node.name)}</span>
73 <br />
74 %endif
64 <p class="help-block">${node.description}</p>
75 <p class="help-block">${node.description}</p>
65 </div>
76 </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">
78 ${node.default}
79 <p class="help-block">${node.description}</p>
80 </div>
81 %else:
82 <div class="input">
83 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
87 </div>
77 </div>
88 %endfor
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 <div class="buttons">
86 <div class="buttons">
90 ${h.submit('save',_('Save'),class_="btn")}
87 ${h.submit('save',_('Save'),class_="btn")}
91 </div>
88 </div>
89
92 </div>
90 </div>
93 ${h.end_form()}
91 ${h.end_form()}
94 </div>
92 </div>
@@ -66,7 +66,7 b''
66 %if c.gist.gist_expires == -1:
66 %if c.gist.gist_expires == -1:
67 ${_('never')}
67 ${_('never')}
68 %else:
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 %endif
70 %endif
71 </span>
71 </span>
72 </div>
72 </div>
@@ -29,7 +29,11 b''
29 <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('My Profile')}</a></li>
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 <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li>
30 <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li>
31 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
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 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li>
37 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li>
34 <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li>
38 <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li>
35 <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li>
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 ${_('expires')}: ${_('never')}
42 ${_('expires')}: ${_('never')}
43 %else:
43 %else:
44 %if auth_token.expired:
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 %else:
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 %endif
48 %endif
49 %endif
49 %endif
50 </td>
50 </td>
@@ -54,9 +54,8 b''
54 setTimeout(function(){
54 setTimeout(function () {
55 // we might have a backend problem, try dashboard again
55 // we might have a backend problem, try dashboard again
56 window.location = "${h.url('summary_home', repo_name = c.repo)}";
56 window.location = "${h.url('summary_home', repo_name = c.repo)}";
57 }, 1000);
57 }, 3000);
58 }
58 } else {
59
60 if (skipCheck || jsonResponse.result === true) {
59 if (skipCheck || jsonResponse.result === true) {
61 // success, means go to dashboard
60 // success, means go to dashboard
62 window.location = "${h.url('summary_home', repo_name = c.repo)}";
61 window.location = "${h.url('summary_home', repo_name = c.repo)}";
@@ -65,6 +64,7 b''
65 setTimeout(worker, 1000);
64 setTimeout(worker, 1000);
66 }
65 }
67 }
66 }
67 }
68 else {
68 else {
69 window.location = "${h.url('home')}";
69 window.location = "${h.url('home')}";
70 }
70 }
@@ -43,11 +43,14 b''
43 </div>
43 </div>
44 <div class="field">
44 <div class="field">
45 <div class="label">
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 </div>
50 </div>
48 <div class="select side-by-side-selector">
51 <div class="select side-by-side-selector">
49 <div class="left-group">
52 <div class="left-group">
50 <label class="text" >${_('Chosen group members')}</label>
53 <label class="text"><strong>${_('Chosen group members')}</strong></label>
51 ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)}
54 ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)}
52 <div class="btn" id="remove_all_elements" >
55 <div class="btn" id="remove_all_elements" >
53 ${_('Remove all elements')}
56 ${_('Remove all elements')}
@@ -60,7 +63,8 b''
60 <i id="remove_element" class="icon-chevron-right"></i>
63 <i id="remove_element" class="icon-chevron-right"></i>
61 </div>
64 </div>
62 <div class="right-group">
65 <div class="right-group">
63 <label class="text" >${_('Available members')}</label>
66 <label class="text" >${_('Available users')}
67 </label>
64 ${h.select('available_members',[],c.available_members,multiple=True,size=8,)}
68 ${h.select('available_members',[],c.available_members,multiple=True,size=8,)}
65 <div class="btn" id="add_all_elements" >
69 <div class="btn" id="add_all_elements" >
66 <i class="icon-chevron-left"></i>${_('Add all elements')}
70 <i class="icon-chevron-left"></i>${_('Add all elements')}
@@ -86,6 +90,42 b''
86 'dropdownAutoWidth': true
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 UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
129 UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
90 })
130 })
91 </script>
131 </script>
@@ -38,9 +38,9 b''
38 ${_('expires')}: ${_('never')}
38 ${_('expires')}: ${_('never')}
39 %else:
39 %else:
40 %if auth_token.expired:
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 %else:
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 %endif
44 %endif
45 %endif
45 %endif
46 </td>
46 </td>
@@ -297,7 +297,7 b''
297 <div id="quick_login">
297 <div id="quick_login">
298 %if c.rhodecode_user.username == h.DEFAULT_USER:
298 %if c.rhodecode_user.username == h.DEFAULT_USER:
299 <h4>${_('Sign in to your account')}</h4>
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 <div class="form form-vertical">
301 <div class="form form-vertical">
302 <div class="fields">
302 <div class="fields">
303 <div class="field">
303 <div class="field">
@@ -312,7 +312,7 b''
312 <div class="field">
312 <div class="field">
313 <div class="label">
313 <div class="label">
314 <label for="password">${_('Password')}:</label>
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 </div>
316 </div>
317 <div class="input">
317 <div class="input">
318 ${h.password('password',class_='focus',tabindex=2)}
318 ${h.password('password',class_='focus',tabindex=2)}
@@ -321,7 +321,7 b''
321 <div class="buttons">
321 <div class="buttons">
322 <div class="register">
322 <div class="register">
323 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
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 %endif
325 %endif
326 </div>
326 </div>
327 <div class="submit">
327 <div class="submit">
@@ -341,7 +341,7 b''
341 <ol class="links">
341 <ol class="links">
342 <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li>
342 <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li>
343 <li class="logout">
343 <li class="logout">
344 ${h.secure_form(h.url('logout_home'))}
344 ${h.secure_form(h.route_path('logout'))}
345 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
345 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
346 ${h.end_form()}
346 ${h.end_form()}
347 </li>
347 </li>
@@ -455,6 +455,9 b''
455 tmpl += '<i class="icon-unlock-alt"></i> ';
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 if(obj_dict && state.type == 'group'){
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 }
@@ -496,7 +499,7 b''
496 query.callback({results: cachedData.results});
499 query.callback({results: cachedData.results});
497 } else {
500 } else {
498 $.ajax({
501 $.ajax({
499 url: "${h.url('repo_switcher_data')}",
502 url: "${h.url('goto_switcher_data')}",
500 data: {'query': query.term},
503 data: {'query': query.term},
501 dataType: 'json',
504 dataType: 'json',
502 type: 'GET',
505 type: 'GET',
@@ -514,7 +517,7 b''
514
517
515 $("#repo_switcher").on('select2-selecting', function(e){
518 $("#repo_switcher").on('select2-selecting', function(e){
516 e.preventDefault();
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 ## Global mouse bindings ##
523 ## Global mouse bindings ##
@@ -4,7 +4,6 b''
4 ## ${p.perms_summary(c.perm_user.permissions)}
4 ## ${p.perms_summary(c.perm_user.permissions)}
5
5
6 <%def name="perms_summary(permissions, show_all=False, actions=True)">
6 <%def name="perms_summary(permissions, show_all=False, actions=True)">
7
8 <div id="perms" class="table fields">
7 <div id="perms" class="table fields">
9 %for section in sorted(permissions.keys()):
8 %for section in sorted(permissions.keys()):
10 <div class="panel panel-default">
9 <div class="panel panel-default">
@@ -134,7 +133,15 b''
134 %endif
133 %endif
135 </td>
134 </td>
136 <td class="td-tags">
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 <span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span>
143 <span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span>
144 %endif
138 </td>
145 </td>
139 %if actions:
146 %if actions:
140 <td class="td-action">
147 <td class="td-action">
@@ -115,6 +115,7 b''
115 <!--[if lt IE 9]>
115 <!--[if lt IE 9]>
116 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
116 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
117 <![endif]-->
117 <![endif]-->
118 <script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
118 <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
119 <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
119 <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script>
120 <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script>
120
121
@@ -12,7 +12,7 b''
12 ${base.gravatar_with_user(comment.author.email, 16)}
12 ${base.gravatar_with_user(comment.author.email, 16)}
13 </div>
13 </div>
14 <div class="date">
14 <div class="date">
15 ${h.age_component(comment.modified_at)}
15 ${h.age_component(comment.modified_at, time_is_local=True)}
16 </div>
16 </div>
17 <div class="status-change">
17 <div class="status-change">
18 %if comment.pull_request:
18 %if comment.pull_request:
@@ -80,7 +80,7 b''
80 ${base.gravatar_with_user(comment.author.email, 16)}
80 ${base.gravatar_with_user(comment.author.email, 16)}
81 </div>
81 </div>
82 <div class="date">
82 <div class="date">
83 ${h.age_component(comment.modified_at)}
83 ${h.age_component(comment.modified_at, time_is_local=True)}
84 </div>
84 </div>
85 %if comment.status_change:
85 %if comment.status_change:
86 <span class="changeset-status-container">
86 <span class="changeset-status-container">
@@ -243,7 +243,7 b''
243
243
244 <%def name="gist_created(created_on)">
244 <%def name="gist_created(created_on)">
245 <div class="created">
245 <div class="created">
246 ${h.age_component(created_on)}
246 ${h.age_component(created_on, time_is_local=True)}
247 </div>
247 </div>
248 </%def>
248 </%def>
249
249
@@ -252,7 +252,7 b''
252 %if expires == -1:
252 %if expires == -1:
253 ${_('never')}
253 ${_('never')}
254 %else:
254 %else:
255 ${h.age_component(h.time_to_datetime(expires))}
255 ${h.age_component(h.time_to_utcdatetime(expires))}
256 %endif
256 %endif
257 </div>
257 </div>
258 </%def>
258 </%def>
@@ -289,7 +289,7 b''
289 </%def>
289 </%def>
290
290
291 <%def name="pullrequest_updated_on(updated_on)">
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 </%def>
293 </%def>
294
294
295 <%def name="pullrequest_author(full_contact)">
295 <%def name="pullrequest_author(full_contact)">
@@ -11,7 +11,7 b''
11 ${base.gravatar_with_user(f.user.email, 16)}
11 ${base.gravatar_with_user(f.user.email, 16)}
12 </td>
12 </td>
13 <td class="td-time follower_date">
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 </td>
15 </td>
16 </tr>
16 </tr>
17 % endfor
17 % endfor
@@ -22,7 +22,7 b''
22 <div class="truncate">${f.description}</div>
22 <div class="truncate">${f.description}</div>
23 </td>
23 </td>
24 <td class="td-time follower_date">
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 </td>
26 </td>
27 <td class="td-compare">
27 <td class="td-compare">
28 <a title="${_('Compare fork with %s' % c.repo_name)}"
28 <a title="${_('Compare fork with %s' % c.repo_name)}"
@@ -28,7 +28,7 b''
28 </div>
28 </div>
29 <div class="journal_action_params">${h.literal(h.action_parser(entry)[1]())}</div>
29 <div class="journal_action_params">${h.literal(h.action_parser(entry)[1]())}</div>
30 <div class="date">
30 <div class="date">
31 ${h.age_component(entry.action_date)}
31 ${h.age_component(entry.action_date, time_is_local=True)}
32 </div>
32 </div>
33 %endfor
33 %endfor
34 </div>
34 </div>
@@ -1,6 +1,5 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base/root.html"/>
2 <%inherit file="base/root.html"/>
3 <%namespace file="base/social_buttons.html" import="render_social_buttons"/>
4
3
5 <%def name="title()">
4 <%def name="title()">
6 ${_('Sign In')}
5 ${_('Sign In')}
@@ -35,21 +34,35 b''
35 <div class="sign-in-title">
34 <div class="sign-in-title">
36 <h1>${_('Sign In')}</h1>
35 <h1>${_('Sign In')}</h1>
37 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
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 %endif
38 %endif
40 </div>
39 </div>
41 <div class="inner form">
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 <label for="username">${_('Username')}:</label>
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 <label for="password">${_('Password')}:</label>
50 <label for="password">${_('Password')}:</label>
46 ${h.password('password',class_='focus')}
51 ${h.password('password', class_='focus')}
47 <input type="checkbox" id="remember" name="remember" />
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 <label class="checkbox" for="remember">${_('Remember me')}</label>
58 <label class="checkbox" for="remember">${_('Remember me')}</label>
59
49 <p class="links">
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 </p>
62 </p>
63
52 ${h.submit('sign_in',_('Sign In'),class_="btn sign-in")}
64 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in")}
65
53 ${h.end_form()}
66 ${h.end_form()}
54 <script type="text/javascript">
67 <script type="text/javascript">
55 $(document).ready(function(){
68 $(document).ready(function(){
@@ -57,16 +70,8 b''
57 })
70 })
58 </script>
71 </script>
59 </div>
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 <!-- end login -->
73 <!-- end login -->
74 <%block name="below_login_button" />
70 </div>
75 </div>
71 </div>
76 </div>
72 </div>
77 </div>
@@ -33,18 +33,26 b''
33 <!-- login -->
33 <!-- login -->
34 <div class="sign-in-title">
34 <div class="sign-in-title">
35 <h1>${_('Reset your Password')}</h1>
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 </div>
37 </div>
38 <div class="inner form">
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 <label for="email">${_('Email Address')}:</label>
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 <div class="login-captcha"
48 <div class="login-captcha"
45 <label for="email">${_('Captcha')}:</label>
49 <label for="email">${_('Captcha')}:</label>
46 ${h.hidden('recaptcha_field')}
50 ${h.hidden('recaptcha_field')}
47 <div id="recaptcha"></div>
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 </div>
56 </div>
49 %endif
57 %endif
50
58
@@ -57,14 +65,14 b''
57 </div>
65 </div>
58 </div>
66 </div>
59
67
60 %if c.captcha_active:
68 %if captcha_active:
61 <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
69 <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
62 %endif
70 %endif
63 <script type="text/javascript">
71 <script type="text/javascript">
64 $(document).ready(function(){
72 $(document).ready(function(){
65 $('#email').focus();
73 $('#email').focus();
66 %if c.captcha_active:
74 %if captcha_active:
67 Recaptcha.create("${c.captcha_public_key}", "recaptcha", {theme: "white"});
75 Recaptcha.create("${captcha_public_key}", "recaptcha", {theme: "white"});
68 %endif
76 %endif
69 });
77 });
70 </script> No newline at end of file
78 </script>
@@ -271,7 +271,8 b''
271 'source_ref_type': 'rev',
271 'source_ref_type': 'rev',
272 'target_ref': sourceRef[2],
272 'target_ref': sourceRef[2],
273 'target_ref_type': 'rev',
273 'target_ref_type': 'rev',
274 'merge': true
274 'merge': true,
275 '_': Date.now() // bypass browser caching
275 }; // gather the source/target ref and repo here
276 }; // gather the source/target ref and repo here
276
277
277 if (sourceRef.length !== 3 || targetRef.length !== 3) {
278 if (sourceRef.length !== 3 || targetRef.length !== 3) {
@@ -1,6 +1,5 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base/root.html"/>
2 <%inherit file="base/root.html"/>
3 <%namespace file="base/social_buttons.html" import="render_social_buttons"/>
4
3
5 <%def name="title()">
4 <%def name="title()">
6 ${_('Create an Account')}
5 ${_('Create an Account')}
@@ -34,65 +33,91 b''
34 <!-- login -->
33 <!-- login -->
35 <div class="sign-in-title">
34 <div class="sign-in-title">
36 <h1>${_('Create an account')}</h1>
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 </div>
37 </div>
39 <div class="inner form">
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 <label for="username">${_('Username')}:</label>
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 <label for="password">${_('Password')}:</label>
48 <label for="password">${_('Password')}:</label>
44 ${h.password('password', c.form_data.get('password'))}
49 ${h.password('password', defaults.get('password'))}
45 <label for="password">${_('Re-enter password')}:</label>
50 %if 'password' in errors:
46 ${h.password('password_confirmation', c.form_data.get('password'))}
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 <label for="firstname">${_('First Name')}:</label>
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 <label for="lastname">${_('Last Name')}:</label>
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 <label for="email">${_('Email')}:</label>
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 <div>
84 <div>
56 <label for="email">${_('Captcha')}:</label>
85 <label for="recaptcha">${_('Captcha')}:</label>
57 ${h.hidden('recaptcha_field')}
86 ${h.hidden('recaptcha_field')}
58 <div id="recaptcha"></div>
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 </div>
92 </div>
60 %endif
93 %endif
61
94
62 %if not c.auto_active:
95 %if not auto_active:
63 <p class="activation_msg">
96 <p class="activation_msg">
64 ${_('Account activation requires admin approval.')}
97 ${_('Account activation requires admin approval.')}
65 </p>
98 </p>
66 %endif
99 %endif
67 <p class="register_message">
100 <p class="register_message">
68 ${c.register_message|n}
101 ${register_message|n}
69 </p>
102 </p>
70
103
71 ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")}
104 ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")}
72
105
73 ${h.end_form()}
106 ${h.end_form()}
74 </div>
107 </div>
75
108 <%block name="below_register_button" />
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
84 </div>
109 </div>
85 </div>
110 </div>
86 </div>
111 </div>
87
112
88 %if c.captcha_active:
113 %if captcha_active:
89 <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
114 <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
90 %endif
115 %endif
91 <script type="text/javascript">
116 <script type="text/javascript">
92 $(document).ready(function(){
117 $(document).ready(function(){
93 $('#username').focus();
118 $('#username').focus();
94 %if c.captcha_active:
119 %if captcha_active:
95 Recaptcha.create("${c.captcha_public_key}", "recaptcha", {theme: "white"});
120 Recaptcha.create("${captcha_public_key}", "recaptcha", {theme: "white"});
96 %endif
121 %endif
97 });
122 });
98 </script>
123 </script>
@@ -6,7 +6,13 b''
6 <th>${_('Commit')}</th>
6 <th>${_('Commit')}</th>
7 <th></th>
7 <th></th>
8 <th>${_('Commit message')}</th>
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 <th>${_('Author')}</th>
16 <th>${_('Author')}</th>
11 </tr>
17 </tr>
12 %for entry in c.formatted_results:
18 %for entry in c.formatted_results:
@@ -33,14 +39,14 b''
33 </div>
39 </div>
34 </td>
40 </td>
35 <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">
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 ${h.literal(entry['message_hl'])}
43 ${h.literal(entry['message_hl'])}
38 %else:
44 %else:
39 ${h.urlify_commit_message(entry['message'], entry['repository'])}
45 ${h.urlify_commit_message(entry['message'], entry['repository'])}
40 %endif
46 %endif
41 </td>
47 </td>
42 <td class="td-time">
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 </td>
50 </td>
45
51
46 <td class="td-user author">
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 <div class="search-results">
38 <div class="search-results">
2 %for entry in c.formatted_results:
39 %for entry in c.formatted_results:
3 ## search results are additionally filtered, and this check is just a safe gate
40 ## search results are additionally filtered, and this check is just a safe gate
@@ -38,7 +75,9 b''
38 </div>
75 </div>
39 </div>
76 </div>
40 <div class="code-body search-code-body">
77 <div class="code-body search-code-body">
41 <pre>${h.literal(entry['content_short_hl'])}</pre>
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'))}
42 </div>
81 </div>
43 </div>
82 </div>
44 % endif
83 % endif
@@ -49,3 +88,14 b''
49 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
88 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
50 </div>
89 </div>
51 %endif
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 import pytest
43 import pytest
44
44
45 from rhodecode import is_windows
45 from rhodecode import is_windows
46 from rhodecode.config.routing import ADMIN_PREFIX
46 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
47 from rhodecode.model.db import User
48 from rhodecode.model.db import User
48 from rhodecode.lib import auth
49 from rhodecode.lib import auth
49 from rhodecode.lib.helpers import flash, link_to
50 from rhodecode.lib.helpers import flash, link_to
50 from rhodecode.lib.utils2 import safe_unicode, safe_str
51 from rhodecode.lib.utils2 import safe_unicode, safe_str
52 from rhodecode.tests.utils import get_session_from_response
51
53
52 # TODO: johbo: Solve time zone related issues and remove this tweak
54 # TODO: johbo: Solve time zone related issues and remove this tweak
53 os.environ['TZ'] = 'UTC'
55 os.environ['TZ'] = 'UTC'
@@ -177,26 +179,29 b' class TestController(object):'
177
179
178 def login_user_session(
180 def login_user_session(
179 app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
181 app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
180 response = app.post(url(controller='login', action='index'),
182 from rhodecode.tests.functional.test_login import login_url
181 {'username': username,
183 response = app.post(
182 'password': password})
184 login_url,
183
185 {'username': username, 'password': password})
184 if 'invalid user name' in response.body:
186 if 'invalid user name' in response.body:
185 pytest.fail('could not login using %s %s' % (username, password))
187 pytest.fail('could not login using %s %s' % (username, password))
186
188
187 assert response.status == '302 Found'
189 assert response.status == '302 Found'
188 ses = response.session['rhodecode_user']
189 assert ses.get('username') == username
190 response = response.follow()
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 def logout_user_session(app, csrf_token):
202 def logout_user_session(app, csrf_token):
197 app.post(
203 from rhodecode.tests.functional.test_login import logut_url
198 url(controller='login', action='logout'),
204 app.post(logut_url, {'csrf_token': csrf_token}, status=302)
199 {'csrf_token': csrf_token}, status=302)
200
205
201
206
202 def login_user(app, username=TEST_USER_ADMIN_LOGIN,
207 def login_user(app, username=TEST_USER_ADMIN_LOGIN,
@@ -20,7 +20,8 b''
20
20
21 import pytest
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 from rhodecode.model.db import Session
25 from rhodecode.model.db import Session
25 from rhodecode.model.settings import SettingsModel
26 from rhodecode.model.settings import SettingsModel
26
27
@@ -150,12 +151,14 b' class TestAuthSettingsController(object)'
150 'egg:rhodecode-enterprise-ce#rhodecode,'
151 'egg:rhodecode-enterprise-ce#rhodecode,'
151 'egg:rhodecode-enterprise-ce#ldap',
152 'egg:rhodecode-enterprise-ce#ldap',
152 csrf_token)
153 csrf_token)
154 invalid_port_value = 'invalid-port-number'
153 response = self._post_ldap_settings(params, override={
155 response = self._post_ldap_settings(params, override={
154 'port': 'invalid-port-number',
156 'port': invalid_port_value,
155 })
157 })
156 response.mustcontain(
158 assertr = AssertResponse(response)
157 '<span class="error-message">&quot;invalid-port-number&quot;'
159 assertr.element_contains(
158 ' is not a number</span>')
160 '.form .field #port ~ .error-message',
161 invalid_port_value)
159
162
160 def test_ldap_error_form(self, csrf_token):
163 def test_ldap_error_form(self, csrf_token):
161 params = self._enable_plugins(
164 params = self._enable_plugins(
@@ -339,53 +339,3 b' class TestMyAccountController(TestContro'
339 new_password_hash = response.session['rhodecode_user']['password']
339 new_password_hash = response.session['rhodecode_user']['password']
340
340
341 assert old_password_hash != new_password_hash
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 import pytest
22 import pytest
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.config.routing import ADMIN_PREFIX
25 from rhodecode.lib.utils2 import md5
26 from rhodecode.lib.utils2 import md5
26 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.meta import Session
28 from rhodecode.model.meta import Session
@@ -157,7 +158,7 b' class TestAdminSettingsGlobal:'
157 'csrf_token': csrf_token,
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 response.mustcontain('captcha')
162 response.mustcontain('captcha')
162
163
163 def test_captcha_deactivate(self, csrf_token):
164 def test_captcha_deactivate(self, csrf_token):
@@ -167,7 +168,7 b' class TestAdminSettingsGlobal:'
167 'csrf_token': csrf_token,
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 response.mustcontain(no=['captcha'])
172 response.mustcontain(no=['captcha'])
172
173
173 def test_title_change(self, csrf_token):
174 def test_title_change(self, csrf_token):
@@ -35,7 +35,8 b' class TestAdminUsersGroupsController(Tes'
35
35
36 def test_index(self):
36 def test_index(self):
37 self.log_user()
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 def test_create(self):
41 def test_create(self):
41 self.log_user()
42 self.log_user()
@@ -148,7 +149,19 b' class TestAdminUsersGroupsController(Tes'
148 fixture.destroy_user_group(users_group_name)
149 fixture.destroy_user_group(users_group_name)
149
150
150 def test_edit(self):
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 def test_usergroup_escape(self):
166 def test_usergroup_escape(self):
154 user = User.get_by_username('test_admin')
167 user = User.get_by_username('test_admin')
@@ -77,7 +77,7 b' class TestCompareController:'
77 'hg': {
77 'hg': {
78 'tag': 'v0.2.0',
78 'tag': 'v0.2.0',
79 'branch': 'default',
79 'branch': 'default',
80 'response': (147, 5700, 10176)
80 'response': (147, 5701, 10177)
81 },
81 },
82 'git': {
82 'git': {
83 'tag': 'v0.2.2',
83 'tag': 'v0.2.2',
@@ -181,19 +181,25 b' class TestUserAutocompleteData(TestContr'
181 def assert_and_get_content(result):
181 def assert_and_get_content(result):
182 repos = []
182 repos = []
183 groups = []
183 groups = []
184 commits = []
184 for data in result:
185 for data in result:
185 for data_item in data['children']:
186 for data_item in data['children']:
186 assert data_item['id']
187 assert data_item['id']
187 assert data_item['text']
188 assert data_item['text']
189 assert data_item['url']
188 if data_item['type'] == 'repo':
190 if data_item['type'] == 'repo':
189 repos.append(data_item)
191 repos.append(data_item)
190 else:
192 elif data_item['type'] == 'group':
191 groups.append(data_item)
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 required_repos_with_groups = [
203 required_repos_with_groups = [
198 'abc',
204 'abc',
199 'abc-fork',
205 'abc-fork',
@@ -253,39 +259,41 b' class TestRepoSwitcherData(TestControlle'
253 self.log_user()
259 self.log_user()
254
260
255 response = self.app.get(
261 response = self.app.get(
256 url(controller='home', action='repo_switcher_data'),
262 url(controller='home', action='goto_switcher_data'),
257 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
263 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
258 result = json.loads(response.body)['results']
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 assert len(repos) == len(Repository.get_all())
268 assert len(repos) == len(Repository.get_all())
263 assert len(groups) == len(RepoGroup.get_all())
269 assert len(groups) == len(RepoGroup.get_all())
270 assert len(commits) == 0
264
271
265 def test_returns_list_of_repos_and_groups_filtered(self):
272 def test_returns_list_of_repos_and_groups_filtered(self):
266 self.log_user()
273 self.log_user()
267
274
268 response = self.app.get(
275 response = self.app.get(
269 url(controller='home', action='repo_switcher_data'),
276 url(controller='home', action='goto_switcher_data'),
270 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
277 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
271 params={'query': 'abc'}, status=200)
278 params={'query': 'abc'}, status=200)
272 result = json.loads(response.body)['results']
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 assert len(repos) == 13
283 assert len(repos) == 13
277 assert len(groups) == 5
284 assert len(groups) == 5
285 assert len(commits) == 0
278
286
279 def test_returns_list_of_properly_sorted_and_filtered(self):
287 def test_returns_list_of_properly_sorted_and_filtered(self):
280 self.log_user()
288 self.log_user()
281
289
282 response = self.app.get(
290 response = self.app.get(
283 url(controller='home', action='repo_switcher_data'),
291 url(controller='home', action='goto_switcher_data'),
284 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
292 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
285 params={'query': 'abc'}, status=200)
293 params={'query': 'abc'}, status=200)
286 result = json.loads(response.body)['results']
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 test_repos = [x['text'] for x in repos[:4]]
298 test_repos = [x['text'] for x in repos[:4]]
291 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
299 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
@@ -300,54 +308,58 b' class TestRepoListData(TestController):'
300 self.log_user()
308 self.log_user()
301
309
302 response = self.app.get(
310 response = self.app.get(
303 url(controller='home', action='repo_switcher_data'),
311 url(controller='home', action='repo_list_data'),
304 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
312 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
305 result = json.loads(response.body)['results']
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 assert len(repos) == len(Repository.get_all())
317 assert len(repos) == len(Repository.get_all())
310 assert len(groups) == 0
318 assert len(groups) == 0
319 assert len(commits) == 0
311
320
312 def test_returns_list_of_repos_and_groups_filtered(self):
321 def test_returns_list_of_repos_and_groups_filtered(self):
313 self.log_user()
322 self.log_user()
314
323
315 response = self.app.get(
324 response = self.app.get(
316 url(controller='home', action='repo_switcher_data'),
325 url(controller='home', action='repo_list_data'),
317 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
326 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
318 params={'query': 'vcs_test_git'}, status=200)
327 params={'query': 'vcs_test_git'}, status=200)
319 result = json.loads(response.body)['results']
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 assert len(repos) == len(Repository.query().filter(
332 assert len(repos) == len(Repository.query().filter(
324 Repository.repo_name.ilike('%vcs_test_git%')).all())
333 Repository.repo_name.ilike('%vcs_test_git%')).all())
325 assert len(groups) == 0
334 assert len(groups) == 0
335 assert len(commits) == 0
326
336
327 def test_returns_list_of_repos_and_groups_filtered_with_type(self):
337 def test_returns_list_of_repos_and_groups_filtered_with_type(self):
328 self.log_user()
338 self.log_user()
329
339
330 response = self.app.get(
340 response = self.app.get(
331 url(controller='home', action='repo_switcher_data'),
341 url(controller='home', action='repo_list_data'),
332 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
342 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
333 params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200)
343 params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200)
334 result = json.loads(response.body)['results']
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 assert len(repos) == len(Repository.query().filter(
348 assert len(repos) == len(Repository.query().filter(
339 Repository.repo_name.ilike('%vcs_test_git%')).all())
349 Repository.repo_name.ilike('%vcs_test_git%')).all())
340 assert len(groups) == 0
350 assert len(groups) == 0
351 assert len(commits) == 0
341
352
342 def test_returns_list_of_repos_non_ascii_query(self):
353 def test_returns_list_of_repos_non_ascii_query(self):
343 self.log_user()
354 self.log_user()
344 response = self.app.get(
355 response = self.app.get(
345 url(controller='home', action='repo_switcher_data'),
356 url(controller='home', action='repo_list_data'),
346 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
357 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
347 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200)
358 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200)
348 result = json.loads(response.body)['results']
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 assert len(repos) == 0
363 assert len(repos) == 0
353 assert len(groups) == 0
364 assert len(groups) == 0
365 assert len(commits) == 0
@@ -23,9 +23,11 b' import urlparse'
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.tests import (
27 from rhodecode.tests import (
27 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN)
28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN)
28 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.utils import AssertResponse, get_session_from_response
29 from rhodecode.lib.auth import check_password, generate_auth_token
31 from rhodecode.lib.auth import check_password, generate_auth_token
30 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
31 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.auth_token import AuthTokenModel
@@ -35,6 +37,14 b' from rhodecode.model.meta import Session'
35
37
36 fixture = Fixture()
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 @pytest.mark.usefixtures('app')
49 @pytest.mark.usefixtures('app')
40 class TestLoginController:
50 class TestLoginController:
@@ -52,37 +62,38 b' class TestLoginController:'
52 assert Notification.query().all() == []
62 assert Notification.query().all() == []
53
63
54 def test_index(self):
64 def test_index(self):
55 response = self.app.get(url(controller='login', action='index'))
65 response = self.app.get(login_url)
56 assert response.status == '200 OK'
66 assert response.status == '200 OK'
57 # Test response...
67 # Test response...
58
68
59 def test_login_admin_ok(self):
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 {'username': 'test_admin',
71 {'username': 'test_admin',
62 'password': 'test12'})
72 'password': 'test12'})
63 assert response.status == '302 Found'
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 assert username == 'test_admin'
76 assert username == 'test_admin'
66 response = response.follow()
77 response = response.follow()
67 response.mustcontain('/%s' % HG_REPO)
78 response.mustcontain('/%s' % HG_REPO)
68
79
69 def test_login_regular_ok(self):
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 {'username': 'test_regular',
82 {'username': 'test_regular',
72 'password': 'test12'})
83 'password': 'test12'})
73
84
74 assert response.status == '302 Found'
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 assert username == 'test_regular'
88 assert username == 'test_regular'
77 response = response.follow()
89 response = response.follow()
78 response.mustcontain('/%s' % HG_REPO)
90 response.mustcontain('/%s' % HG_REPO)
79
91
80 def test_login_ok_came_from(self):
92 def test_login_ok_came_from(self):
81 test_came_from = '/_admin/users?branch=stable'
93 test_came_from = '/_admin/users?branch=stable'
82 response = self.app.post(url(controller='login', action='index',
94 _url = '{}?came_from={}'.format(login_url, test_came_from)
83 came_from=test_came_from),
95 response = self.app.post(
84 {'username': 'test_admin',
96 _url, {'username': 'test_admin', 'password': 'test12'})
85 'password': 'test12'})
86 assert response.status == '302 Found'
97 assert response.status == '302 Found'
87 assert 'branch=stable' in response.location
98 assert 'branch=stable' in response.location
88 response = response.follow()
99 response = response.follow()
@@ -100,33 +111,30 b' class TestLoginController:'
100 assert 'branch=stable' in response_query[0][1]
111 assert 'branch=stable' in response_query[0][1]
101
112
102 def test_login_form_with_get_args(self):
113 def test_login_form_with_get_args(self):
103 kwargs = {'branch': 'stable'}
114 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
104 response = self.app.get(
115 response = self.app.get(_url)
105 url(controller='login', action='index',
116 assert 'branch%3Dstable' in response.form.action
106 came_from='/_admin/users', **kwargs))
107 assert 'branch=stable' in response.form.action
108
117
109 @pytest.mark.parametrize("url_came_from", [
118 @pytest.mark.parametrize("url_came_from", [
110 ('data:text/html,<script>window.alert("xss")</script>',),
119 'data:text/html,<script>window.alert("xss")</script>',
111 ('mailto:test@rhodecode.org',),
120 'mailto:test@rhodecode.org',
112 ('file:///etc/passwd',),
121 'file:///etc/passwd',
113 ('ftp://some.ftp.server',),
122 'ftp://some.ftp.server',
114 ('http://other.domain',),
123 'http://other.domain',
115 ('/\r\nX-Forwarded-Host: http://example.org',),
124 '/\r\nX-Forwarded-Host: http://example.org',
116 ])
125 ])
117 def test_login_bad_came_froms(self, url_came_from):
126 def test_login_bad_came_froms(self, url_came_from):
118 response = self.app.post(url(controller='login', action='index',
127 _url = '{}?came_from={}'.format(login_url, url_came_from)
119 came_from=url_came_from),
128 response = self.app.post(
120 {'username': 'test_admin',
129 _url,
121 'password': 'test12'})
130 {'username': 'test_admin', 'password': 'test12'})
122 assert response.status == '302 Found'
131 assert response.status == '302 Found'
123 assert response.tmpl_context.came_from == '/'
124
125 response = response.follow()
132 response = response.follow()
126 assert response.status == '200 OK'
133 assert response.status == '200 OK'
134 assert response.request.path == '/'
127
135
128 def test_login_short_password(self):
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 {'username': 'test_admin',
138 {'username': 'test_admin',
131 'password': 'as'})
139 'password': 'as'})
132 assert response.status == '200 OK'
140 assert response.status == '200 OK'
@@ -135,7 +143,7 b' class TestLoginController:'
135
143
136 def test_login_wrong_non_ascii_password(self, user_regular):
144 def test_login_wrong_non_ascii_password(self, user_regular):
137 response = self.app.post(
145 response = self.app.post(
138 url(controller='login', action='index'),
146 login_url,
139 {'username': user_regular.username,
147 {'username': user_regular.username,
140 'password': u'invalid-non-asci\xe4'.encode('utf8')})
148 'password': u'invalid-non-asci\xe4'.encode('utf8')})
141
149
@@ -146,13 +154,13 b' class TestLoginController:'
146 password = u'valid-non-ascii\xe4'
154 password = u'valid-non-ascii\xe4'
147 user = user_util.create_user(password=password)
155 user = user_util.create_user(password=password)
148 response = self.app.post(
156 response = self.app.post(
149 url(controller='login', action='index'),
157 login_url,
150 {'username': user.username,
158 {'username': user.username,
151 'password': password.encode('utf-8')})
159 'password': password.encode('utf-8')})
152 assert response.status_code == 302
160 assert response.status_code == 302
153
161
154 def test_login_wrong_username_password(self):
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 {'username': 'error',
164 {'username': 'error',
157 'password': 'test12'})
165 'password': 'test12'})
158
166
@@ -170,12 +178,13 b' class TestLoginController:'
170 Session().add(user)
178 Session().add(user)
171 Session().commit()
179 Session().commit()
172 self.destroy_users.add(temp_user)
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 {'username': temp_user,
182 {'username': temp_user,
175 'password': 'test123'})
183 'password': 'test123'})
176
184
177 assert response.status == '302 Found'
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 assert username == temp_user
188 assert username == temp_user
180 response = response.follow()
189 response = response.follow()
181 response.mustcontain('/%s' % HG_REPO)
190 response.mustcontain('/%s' % HG_REPO)
@@ -186,13 +195,13 b' class TestLoginController:'
186
195
187 # REGISTRATIONS
196 # REGISTRATIONS
188 def test_register(self):
197 def test_register(self):
189 response = self.app.get(url(controller='login', action='register'))
198 response = self.app.get(register_url)
190 response.mustcontain('Create an Account')
199 response.mustcontain('Create an Account')
191
200
192 def test_register_err_same_username(self):
201 def test_register_err_same_username(self):
193 uname = 'test_admin'
202 uname = 'test_admin'
194 response = self.app.post(
203 response = self.app.post(
195 url(controller='login', action='register'),
204 register_url,
196 {
205 {
197 'username': uname,
206 'username': uname,
198 'password': 'test12',
207 'password': 'test12',
@@ -203,13 +212,14 b' class TestLoginController:'
203 }
212 }
204 )
213 )
205
214
215 assertr = AssertResponse(response)
206 msg = validators.ValidUsername()._messages['username_exists']
216 msg = validators.ValidUsername()._messages['username_exists']
207 msg = h.html_escape(msg % {'username': uname})
217 msg = msg % {'username': uname}
208 response.mustcontain(msg)
218 assertr.element_contains('#username+.error-message', msg)
209
219
210 def test_register_err_same_email(self):
220 def test_register_err_same_email(self):
211 response = self.app.post(
221 response = self.app.post(
212 url(controller='login', action='register'),
222 register_url,
213 {
223 {
214 'username': 'test_admin_0',
224 'username': 'test_admin_0',
215 'password': 'test12',
225 'password': 'test12',
@@ -220,12 +230,13 b' class TestLoginController:'
220 }
230 }
221 )
231 )
222
232
233 assertr = AssertResponse(response)
223 msg = validators.UniqSystemEmail()()._messages['email_taken']
234 msg = validators.UniqSystemEmail()()._messages['email_taken']
224 response.mustcontain(msg)
235 assertr.element_contains('#email+.error-message', msg)
225
236
226 def test_register_err_same_email_case_sensitive(self):
237 def test_register_err_same_email_case_sensitive(self):
227 response = self.app.post(
238 response = self.app.post(
228 url(controller='login', action='register'),
239 register_url,
229 {
240 {
230 'username': 'test_admin_1',
241 'username': 'test_admin_1',
231 'password': 'test12',
242 'password': 'test12',
@@ -235,12 +246,13 b' class TestLoginController:'
235 'lastname': 'test'
246 'lastname': 'test'
236 }
247 }
237 )
248 )
249 assertr = AssertResponse(response)
238 msg = validators.UniqSystemEmail()()._messages['email_taken']
250 msg = validators.UniqSystemEmail()()._messages['email_taken']
239 response.mustcontain(msg)
251 assertr.element_contains('#email+.error-message', msg)
240
252
241 def test_register_err_wrong_data(self):
253 def test_register_err_wrong_data(self):
242 response = self.app.post(
254 response = self.app.post(
243 url(controller='login', action='register'),
255 register_url,
244 {
256 {
245 'username': 'xs',
257 'username': 'xs',
246 'password': 'test',
258 'password': 'test',
@@ -256,7 +268,7 b' class TestLoginController:'
256
268
257 def test_register_err_username(self):
269 def test_register_err_username(self):
258 response = self.app.post(
270 response = self.app.post(
259 url(controller='login', action='register'),
271 register_url,
260 {
272 {
261 'username': 'error user',
273 'username': 'error user',
262 'password': 'test12',
274 'password': 'test12',
@@ -277,7 +289,7 b' class TestLoginController:'
277 def test_register_err_case_sensitive(self):
289 def test_register_err_case_sensitive(self):
278 usr = 'Test_Admin'
290 usr = 'Test_Admin'
279 response = self.app.post(
291 response = self.app.post(
280 url(controller='login', action='register'),
292 register_url,
281 {
293 {
282 'username': usr,
294 'username': usr,
283 'password': 'test12',
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 msg = validators.ValidUsername()._messages['username_exists']
304 msg = validators.ValidUsername()._messages['username_exists']
293 msg = h.html_escape(msg % {'username': usr})
305 msg = msg % {'username': usr}
294 response.mustcontain(msg)
306 assertr.element_contains('#username+.error-message', msg)
295
307
296 def test_register_special_chars(self):
308 def test_register_special_chars(self):
297 response = self.app.post(
309 response = self.app.post(
298 url(controller='login', action='register'),
310 register_url,
299 {
311 {
300 'username': 'xxxaxn',
312 'username': 'xxxaxn',
301 'password': 'ąćźżąśśśś',
313 'password': 'ąćźżąśśśś',
@@ -311,7 +323,7 b' class TestLoginController:'
311
323
312 def test_register_password_mismatch(self):
324 def test_register_password_mismatch(self):
313 response = self.app.post(
325 response = self.app.post(
314 url(controller='login', action='register'),
326 register_url,
315 {
327 {
316 'username': 'xs',
328 'username': 'xs',
317 'password': '123qwe',
329 'password': '123qwe',
@@ -332,7 +344,7 b' class TestLoginController:'
332 lastname = 'testlastname'
344 lastname = 'testlastname'
333
345
334 response = self.app.post(
346 response = self.app.post(
335 url(controller='login', action='register'),
347 register_url,
336 {
348 {
337 'username': username,
349 'username': username,
338 'password': password,
350 'password': password,
@@ -360,7 +372,7 b' class TestLoginController:'
360 def test_forgot_password_wrong_mail(self):
372 def test_forgot_password_wrong_mail(self):
361 bad_email = 'marcin@wrongmail.org'
373 bad_email = 'marcin@wrongmail.org'
362 response = self.app.post(
374 response = self.app.post(
363 url(controller='login', action='password_reset'),
375 pwd_reset_url,
364 {'email': bad_email, }
376 {'email': bad_email, }
365 )
377 )
366
378
@@ -369,8 +381,7 b' class TestLoginController:'
369 response.mustcontain()
381 response.mustcontain()
370
382
371 def test_forgot_password(self):
383 def test_forgot_password(self):
372 response = self.app.get(url(controller='login',
384 response = self.app.get(pwd_reset_url)
373 action='password_reset'))
374 assert response.status == '200 OK'
385 assert response.status == '200 OK'
375
386
376 username = 'test_password_reset_1'
387 username = 'test_password_reset_1'
@@ -389,8 +400,7 b' class TestLoginController:'
389 Session().add(new)
400 Session().add(new)
390 Session().commit()
401 Session().commit()
391
402
392 response = self.app.post(url(controller='login',
403 response = self.app.post(pwd_reset_url,
393 action='password_reset'),
394 {'email': email, })
404 {'email': email, })
395
405
396 assert_session_flash(
406 assert_session_flash(
@@ -401,20 +411,18 b' class TestLoginController:'
401 # BAD KEY
411 # BAD KEY
402
412
403 key = "bad"
413 key = "bad"
404 response = self.app.get(url(controller='login',
414 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
405 action='password_reset_confirmation',
415 response = self.app.get(confirm_url)
406 key=key))
407 assert response.status == '302 Found'
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 # GOOD KEY
419 # GOOD KEY
411
420
412 key = User.get_by_username(username).api_key
421 key = User.get_by_username(username).api_key
413 response = self.app.get(url(controller='login',
422 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
414 action='password_reset_confirmation',
423 response = self.app.get(confirm_url)
415 key=key))
416 assert response.status == '302 Found'
424 assert response.status == '302 Found'
417 assert response.location.endswith(url('login_home'))
425 assert response.location.endswith(login_url)
418
426
419 assert_session_flash(
427 assert_session_flash(
420 response,
428 response,
@@ -99,12 +99,13 b' class TestPullrequestsController:'
99 in response) != pr_merge_enabled
99 in response) != pr_merge_enabled
100
100
101 def test_close_status_visibility(self, pr_util, csrf_token):
101 def test_close_status_visibility(self, pr_util, csrf_token):
102 from rhodecode.tests.functional.test_login import login_url, logut_url
102 # Logout
103 # Logout
103 response = self.app.post(
104 response = self.app.post(
104 url(controller='login', action='logout'),
105 logut_url,
105 params={'csrf_token': csrf_token})
106 params={'csrf_token': csrf_token})
106 # Login as regular user
107 # Login as regular user
107 response = self.app.post(url(controller='login', action='index'),
108 response = self.app.post(login_url,
108 {'username': 'test_regular',
109 {'username': 'test_regular',
109 'password': 'test12'})
110 'password': 'test12'})
110
111
@@ -129,6 +129,10 b' class TestSearchController(TestControlle'
129 ('author:marcin@python-blog.com '
129 ('author:marcin@python-blog.com '
130 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
130 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
131 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
131 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
132 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
133 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
134 ('b986218b', 1, [
135 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
132 ])
136 ])
133 def test_search_commit_messages(
137 def test_search_commit_messages(
134 self, query, expected_hits, expected_commits, enabled_backends):
138 self, query, expected_hits, expected_commits, enabled_backends):
@@ -155,3 +155,29 b' class TestRhodeCodeAuthPlugin(object):'
155 self.password_generator_mock = password_generator_patch.start()
155 self.password_generator_mock = password_generator_patch.start()
156 self.password_generator_mock.return_value = 'new-password'
156 self.password_generator_mock.return_value = 'new-password'
157 self.finalizers.append(password_generator_patch.stop)
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 from rhodecode.model.user_group import UserGroupModel
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 def test_cached_perms_data(user_regular, backend_random):
65 def test_cached_perms_data(user_regular, backend_random):
36 permissions = get_permissions(user_regular)
66 permissions = get_permissions(user_regular)
37 repo_name = backend_random.repo.repo_name
67 repo_name = backend_random.repo.repo_name
@@ -155,3 +155,42 b' def test_get_visual_attr(pylonsapp):'
155 def test_chop_at(test_text, inclusive, expected_text):
155 def test_chop_at(test_text, inclusive, expected_text):
156 assert helpers.chop_at_smart(
156 assert helpers.chop_at_smart(
157 test_text, '\n', inclusive, '...') == expected_text
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 except urllib2.URLError:
270 except urllib2.URLError:
271 return False
271 return False
272 return True
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 entry_points={
214 entry_points={
215 'enterprise.plugins1': [
215 'enterprise.plugins1': [
216 'crowd=rhodecode.authentication.plugins.auth_crowd:plugin_factory',
216 'crowd=rhodecode.authentication.plugins.auth_crowd:plugin_factory',
217 'headers=rhodecode.authentication.plugins.auth_headers:plugin_factory',
217 'jasig_cas=rhodecode.authentication.plugins.auth_jasig_cas:plugin_factory',
218 'jasig_cas=rhodecode.authentication.plugins.auth_jasig_cas:plugin_factory',
218 'ldap=rhodecode.authentication.plugins.auth_ldap:plugin_factory',
219 'ldap=rhodecode.authentication.plugins.auth_ldap:plugin_factory',
219 'pam=rhodecode.authentication.plugins.auth_pam:plugin_factory',
220 'pam=rhodecode.authentication.plugins.auth_pam:plugin_factory',
220 'rhodecode=rhodecode.authentication.plugins.auth_rhodecode:plugin_factory',
221 'rhodecode=rhodecode.authentication.plugins.auth_rhodecode:plugin_factory',
222 'token=rhodecode.authentication.plugins.auth_token:plugin_factory',
221 ],
223 ],
222 'paste.app_factory': [
224 'paste.app_factory': [
223 'main=rhodecode.config.middleware:make_pyramid_app',
225 'main=rhodecode.config.middleware:make_pyramid_app',
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
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
NO CONTENT: file was removed
1 NO CONTENT: file was removed
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