##// END OF EJS Templates
core: change from homebrew plugin system into pyramid machinery....
marcink -
r3240:b74ad312 default
parent child Browse files
Show More
@@ -1,147 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 from zope.interface import implementer
26 26
27 27 from rhodecode.apps._base.interfaces import IAdminNavigationRegistry
28 28 from rhodecode.lib.utils2 import str2bool
29 29 from rhodecode.translation import _
30 30
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34 NavListEntry = collections.namedtuple(
35 35 'NavListEntry', ['key', 'name', 'url', 'active_list'])
36 36
37 37
38 38 class NavEntry(object):
39 39 """
40 40 Represents an entry in the admin navigation.
41 41
42 42 :param key: Unique identifier used to store reference in an OrderedDict.
43 43 :param name: Display name, usually a translation string.
44 44 :param view_name: Name of the view, used generate the URL.
45 45 :param active_list: list of urls that we select active for this element
46 46 """
47 47
48 48 def __init__(self, key, name, view_name, active_list=None):
49 49 self.key = key
50 50 self.name = name
51 51 self.view_name = view_name
52 52 self._active_list = active_list or []
53 53
54 54 def generate_url(self, request):
55 55 return request.route_path(self.view_name)
56 56
57 57 def get_localized_name(self, request):
58 58 return request.translate(self.name)
59 59
60 60 @property
61 61 def active_list(self):
62 62 active_list = [self.key]
63 63 if self._active_list:
64 64 active_list = self._active_list
65 65 return active_list
66 66
67 67
68 68 @implementer(IAdminNavigationRegistry)
69 69 class NavigationRegistry(object):
70 70
71 71 _base_entries = [
72 72 NavEntry('global', _('Global'),
73 73 'admin_settings_global'),
74 74 NavEntry('vcs', _('VCS'),
75 75 'admin_settings_vcs'),
76 76 NavEntry('visual', _('Visual'),
77 77 'admin_settings_visual'),
78 78 NavEntry('mapping', _('Remap and Rescan'),
79 79 'admin_settings_mapping'),
80 80 NavEntry('issuetracker', _('Issue Tracker'),
81 81 'admin_settings_issuetracker'),
82 82 NavEntry('email', _('Email'),
83 83 'admin_settings_email'),
84 84 NavEntry('hooks', _('Hooks'),
85 85 'admin_settings_hooks'),
86 86 NavEntry('search', _('Full Text Search'),
87 87 'admin_settings_search'),
88 88 NavEntry('integrations', _('Integrations'),
89 89 'global_integrations_home'),
90 90 NavEntry('system', _('System Info'),
91 91 'admin_settings_system'),
92 92 NavEntry('exceptions', _('Exceptions Tracker'),
93 93 'admin_settings_exception_tracker',
94 94 active_list=['exceptions', 'exceptions_browse']),
95 95 NavEntry('process_management', _('Processes'),
96 96 'admin_settings_process_management'),
97 97 NavEntry('sessions', _('User Sessions'),
98 98 'admin_settings_sessions'),
99 99 NavEntry('open_source', _('Open Source Licenses'),
100 100 'admin_settings_open_source'),
101 101 NavEntry('automation', _('Automation'),
102 102 'admin_settings_automation')
103 103 ]
104 104
105 105 _labs_entry = NavEntry('labs', _('Labs'),
106 106 'admin_settings_labs')
107 107
108 108 def __init__(self, labs_active=False):
109 109 self._registered_entries = collections.OrderedDict()
110 110 for item in self.__class__._base_entries:
111 111 self._registered_entries[item.key] = item
112 112
113 113 if labs_active:
114 114 self.add_entry(self._labs_entry)
115 115
116 116 def add_entry(self, entry):
117 117 self._registered_entries[entry.key] = entry
118 118
119 119 def get_navlist(self, request):
120 navlist = [NavListEntry(i.key, i.get_localized_name(request),
121 i.generate_url(request), i.active_list)
122 for i in self._registered_entries.values()]
123 return navlist
120 nav_list = [
121 NavListEntry(i.key, i.get_localized_name(request),
122 i.generate_url(request), i.active_list)
123 for i in self._registered_entries.values()]
124 return nav_list
124 125
125 126
126 127 def navigation_registry(request, registry=None):
127 128 """
128 129 Helper that returns the admin navigation registry.
129 130 """
130 131 pyramid_registry = registry or request.registry
131 132 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
132 133 return nav_registry
133 134
134 135
135 136 def navigation_list(request):
136 137 """
137 138 Helper that returns the admin navigation as list of NavListEntry objects.
138 139 """
139 140 return navigation_registry(request).get_navlist(request)
140 141
141 142
142 143 def includeme(config):
143 144 # Create admin navigation registry and add it to the pyramid registry.
144 145 settings = config.get_settings()
145 146 labs_active = str2bool(settings.get('labs_settings_active', False))
146 navigation_registry = NavigationRegistry(labs_active=labs_active)
147 config.registry.registerUtility(navigation_registry) No newline at end of file
147 navigation_registry_instance = NavigationRegistry(labs_active=labs_active)
148 config.registry.registerUtility(navigation_registry_instance)
@@ -1,137 +1,111 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import os
22 21 import logging
23 22 import importlib
24 23
25 from pkg_resources import iter_entry_points
26 24 from pyramid.authentication import SessionAuthenticationPolicy
27 25
28 26 from rhodecode.authentication.registry import AuthenticationPluginRegistry
29 27 from rhodecode.authentication.routes import root_factory
30 28 from rhodecode.authentication.routes import AuthnRootResource
31 29 from rhodecode.apps._base import ADMIN_PREFIX
32 30 from rhodecode.model.settings import SettingsModel
33 31
34
35 32 log = logging.getLogger(__name__)
36 33
37 # Plugin ID prefixes to distinct between normal and legacy plugins.
38 plugin_prefix = 'egg:'
39 34 legacy_plugin_prefix = 'py:'
40 35 plugin_default_auth_ttl = 30
41 36
42 37
43 # TODO: Currently this is only used to discover the authentication plugins.
44 # Later on this may be used in a generic way to look up and include all kinds
45 # of supported enterprise plugins. Therefore this has to be moved and
46 # refactored to a real 'plugin look up' machinery.
47 # TODO: When refactoring this think about splitting it up into distinct
48 # discover, load and include phases.
49 def _discover_plugins(config, entry_point='enterprise.plugins1'):
50 log.debug('authentication: running plugin discovery for entrypoint %s',
51 entry_point)
52
53 for ep in iter_entry_points(entry_point):
54 plugin_id = '{}{}#{}'.format(
55 plugin_prefix, ep.dist.project_name, ep.name)
56 log.debug('Plugin discovered: "%s"', plugin_id)
57 try:
58 module = ep.load()
59 plugin = module(plugin_id=plugin_id)
60 config.include(plugin.includeme)
61 except Exception as e:
62 log.exception(
63 'Exception while loading authentication plugin '
64 '"{}": {}'.format(plugin_id, e.message))
65
66
67 38 def _import_legacy_plugin(plugin_id):
68 39 module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
69 40 module = importlib.import_module(module_name)
70 41 return module.plugin_factory(plugin_id=plugin_id)
71 42
72 43
73 44 def _discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
74 45 """
75 46 Function that imports the legacy plugins stored in the 'auth_plugins'
76 47 setting in database which are using the specified prefix. Normally 'py:' is
77 48 used for the legacy plugins.
78 49 """
79 50 log.debug('authentication: running legacy plugin discovery for prefix %s',
80 51 legacy_plugin_prefix)
81 52 try:
82 53 auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
83 54 enabled_plugins = auth_plugins.app_settings_value
84 55 legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
85 56 except Exception:
86 57 legacy_plugins = []
87 58
88 59 for plugin_id in legacy_plugins:
89 60 log.debug('Legacy plugin discovered: "%s"', plugin_id)
90 61 try:
91 62 plugin = _import_legacy_plugin(plugin_id)
92 63 config.include(plugin.includeme)
93 64 except Exception as e:
94 65 log.exception(
95 66 'Exception while loading legacy authentication plugin '
96 67 '"{}": {}'.format(plugin_id, e.message))
97 68
98 69
99 70 def includeme(config):
100 71 # Set authentication policy.
101 72 authn_policy = SessionAuthenticationPolicy()
102 73 config.set_authentication_policy(authn_policy)
103 74
104 75 # Create authentication plugin registry and add it to the pyramid registry.
105 76 authn_registry = AuthenticationPluginRegistry(config.get_settings())
106 77 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
107 78 config.registry.registerUtility(authn_registry)
108 79
109 80 # Create authentication traversal root resource.
110 81 authn_root_resource = root_factory()
111 82 config.add_directive('add_authn_resource',
112 83 authn_root_resource.add_authn_resource)
113 84
114 85 # Add the authentication traversal route.
115 86 config.add_route('auth_home',
116 87 ADMIN_PREFIX + '/auth*traverse',
117 88 factory=root_factory)
118 89 # Add the authentication settings root views.
119 90 config.add_view('rhodecode.authentication.views.AuthSettingsView',
120 91 attr='index',
121 92 request_method='GET',
122 93 route_name='auth_home',
123 94 context=AuthnRootResource)
124 95 config.add_view('rhodecode.authentication.views.AuthSettingsView',
125 96 attr='auth_settings',
126 97 request_method='POST',
127 98 route_name='auth_home',
128 99 context=AuthnRootResource)
129 100
130 for key in ['RC_CMD_SETUP_RC', 'RC_CMD_UPGRADE_DB', 'RC_CMD_SSH_WRAPPER']:
131 if os.environ.get(key):
132 # skip this heavy step below on certain CLI commands
133 return
101 # load CE authentication plugins
102 config.include('rhodecode.authentication.plugins.auth_crowd')
103 config.include('rhodecode.authentication.plugins.auth_headers')
104 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
105 config.include('rhodecode.authentication.plugins.auth_ldap')
106 config.include('rhodecode.authentication.plugins.auth_pam')
107 config.include('rhodecode.authentication.plugins.auth_rhodecode')
108 config.include('rhodecode.authentication.plugins.auth_token')
134 109
135 110 # Auto discover authentication plugins and include their configuration.
136 _discover_plugins(config)
137 111 _discover_legacy_plugins(config)
@@ -1,289 +1,294 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for Atlassian CROWD
23 23 """
24 24
25 25
26 26 import colander
27 27 import base64
28 28 import logging
29 29 import urllib2
30 30
31 31 from rhodecode.translation import _
32 32 from rhodecode.authentication.base import (
33 33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36 from rhodecode.lib.colander_utils import strip_whitespace
37 37 from rhodecode.lib.ext_json import json, formatted_json
38 38 from rhodecode.model.db import User
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwds):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 host = colander.SchemaNode(
58 58 colander.String(),
59 59 default='127.0.0.1',
60 60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 61 preparer=strip_whitespace,
62 62 title=_('Host'),
63 63 widget='string')
64 64 port = colander.SchemaNode(
65 65 colander.Int(),
66 66 default=8095,
67 67 description=_('The Port in use by the Atlassian CROWD Server'),
68 68 preparer=strip_whitespace,
69 69 title=_('Port'),
70 70 validator=colander.Range(min=0, max=65536),
71 71 widget='int')
72 72 app_name = colander.SchemaNode(
73 73 colander.String(),
74 74 default='',
75 75 description=_('The Application Name to authenticate to CROWD'),
76 76 preparer=strip_whitespace,
77 77 title=_('Application Name'),
78 78 widget='string')
79 79 app_password = colander.SchemaNode(
80 80 colander.String(),
81 81 default='',
82 82 description=_('The password to authenticate to CROWD'),
83 83 preparer=strip_whitespace,
84 84 title=_('Application Password'),
85 85 widget='password')
86 86 admin_groups = colander.SchemaNode(
87 87 colander.String(),
88 88 default='',
89 89 description=_('A comma separated list of group names that identify '
90 90 'users as RhodeCode Administrators'),
91 91 missing='',
92 92 preparer=strip_whitespace,
93 93 title=_('Admin Groups'),
94 94 widget='string')
95 95
96 96
97 97 class CrowdServer(object):
98 98 def __init__(self, *args, **kwargs):
99 99 """
100 100 Create a new CrowdServer object that points to IP/Address 'host',
101 101 on the given port, and using the given method (https/http). user and
102 102 passwd can be set here or with set_credentials. If unspecified,
103 103 "version" defaults to "latest".
104 104
105 105 example::
106 106
107 107 cserver = CrowdServer(host="127.0.0.1",
108 108 port="8095",
109 109 user="some_app",
110 110 passwd="some_passwd",
111 111 version="1")
112 112 """
113 113 if not "port" in kwargs:
114 114 kwargs["port"] = "8095"
115 115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 117 kwargs.get("host", "127.0.0.1"),
118 118 kwargs.get("port", "8095"))
119 119 self.set_credentials(kwargs.get("user", ""),
120 120 kwargs.get("passwd", ""))
121 121 self._version = kwargs.get("version", "latest")
122 122 self._url_list = None
123 123 self._appname = "crowd"
124 124
125 125 def set_credentials(self, user, passwd):
126 126 self.user = user
127 127 self.passwd = passwd
128 128 self._make_opener()
129 129
130 130 def _make_opener(self):
131 131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 134 self.opener = urllib2.build_opener(handler)
135 135
136 136 def _request(self, url, body=None, headers=None,
137 137 method=None, noformat=False,
138 138 empty_response_ok=False):
139 139 _headers = {"Content-type": "application/json",
140 140 "Accept": "application/json"}
141 141 if self.user and self.passwd:
142 142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 143 _headers["Authorization"] = "Basic %s" % authstring
144 144 if headers:
145 145 _headers.update(headers)
146 146 log.debug("Sent crowd: \n%s"
147 147 % (formatted_json({"url": url, "body": body,
148 148 "headers": _headers})))
149 149 request = urllib2.Request(url, body, _headers)
150 150 if method:
151 151 request.get_method = lambda: method
152 152
153 153 global msg
154 154 msg = ""
155 155 try:
156 156 rdoc = self.opener.open(request)
157 157 msg = "".join(rdoc.readlines())
158 158 if not msg and empty_response_ok:
159 159 rval = {}
160 160 rval["status"] = True
161 161 rval["error"] = "Response body was empty"
162 162 elif not noformat:
163 163 rval = json.loads(msg)
164 164 rval["status"] = True
165 165 else:
166 166 rval = "".join(rdoc.readlines())
167 167 except Exception as e:
168 168 if not noformat:
169 169 rval = {"status": False,
170 170 "body": body,
171 171 "error": str(e) + "\n" + msg}
172 172 else:
173 173 rval = None
174 174 return rval
175 175
176 176 def user_auth(self, username, password):
177 177 """Authenticate a user against crowd. Returns brief information about
178 178 the user."""
179 179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 180 % (self._uri, self._version, username))
181 181 body = json.dumps({"value": password})
182 182 return self._request(url, body)
183 183
184 184 def user_groups(self, username):
185 185 """Retrieve a list of groups to which this user belongs."""
186 186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 187 % (self._uri, self._version, username))
188 188 return self._request(url)
189 189
190 190
191 191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 192 _settings_unsafe_keys = ['app_password']
193 193
194 194 def includeme(self, config):
195 195 config.add_authn_plugin(self)
196 196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 197 config.add_view(
198 198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 199 attr='settings_get',
200 200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 201 request_method='GET',
202 202 route_name='auth_home',
203 203 context=CrowdAuthnResource)
204 204 config.add_view(
205 205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 206 attr='settings_post',
207 207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 208 request_method='POST',
209 209 route_name='auth_home',
210 210 context=CrowdAuthnResource)
211 211
212 212 def get_settings_schema(self):
213 213 return CrowdSettingsSchema()
214 214
215 215 def get_display_name(self):
216 216 return _('CROWD')
217 217
218 218 @classmethod
219 219 def docs(cls):
220 220 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
221 221
222 222 @hybrid_property
223 223 def name(self):
224 224 return "crowd"
225 225
226 226 def use_fake_password(self):
227 227 return True
228 228
229 229 def user_activation_state(self):
230 230 def_user_perms = User.get_default_user().AuthUser().permissions['global']
231 231 return 'hg.extern_activate.auto' in def_user_perms
232 232
233 233 def auth(self, userobj, username, password, settings, **kwargs):
234 234 """
235 235 Given a user object (which may be null), username, a plaintext password,
236 236 and a settings object (containing all the keys needed as listed in settings()),
237 237 authenticate this user's login attempt.
238 238
239 239 Return None on failure. On success, return a dictionary of the form:
240 240
241 241 see: RhodeCodeAuthPluginBase.auth_func_attrs
242 242 This is later validated for correctness
243 243 """
244 244 if not username or not password:
245 245 log.debug('Empty username or password skipping...')
246 246 return None
247 247
248 248 log.debug("Crowd settings: \n%s", formatted_json(settings))
249 249 server = CrowdServer(**settings)
250 250 server.set_credentials(settings["app_name"], settings["app_password"])
251 251 crowd_user = server.user_auth(username, password)
252 252 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
253 253 if not crowd_user["status"]:
254 254 return None
255 255
256 256 res = server.user_groups(crowd_user["name"])
257 257 log.debug("Crowd groups: \n%s", formatted_json(res))
258 258 crowd_user["groups"] = [x["name"] for x in res["groups"]]
259 259
260 260 # old attrs fetched from RhodeCode database
261 261 admin = getattr(userobj, 'admin', False)
262 262 active = getattr(userobj, 'active', True)
263 263 email = getattr(userobj, 'email', '')
264 264 username = getattr(userobj, 'username', username)
265 265 firstname = getattr(userobj, 'firstname', '')
266 266 lastname = getattr(userobj, 'lastname', '')
267 267 extern_type = getattr(userobj, 'extern_type', '')
268 268
269 269 user_attrs = {
270 270 'username': username,
271 271 'firstname': crowd_user["first-name"] or firstname,
272 272 'lastname': crowd_user["last-name"] or lastname,
273 273 'groups': crowd_user["groups"],
274 274 'user_group_sync': True,
275 275 'email': crowd_user["email"] or email,
276 276 'admin': admin,
277 277 'active': active,
278 278 'active_from_extern': crowd_user.get('active'),
279 279 'extern_name': crowd_user["name"],
280 280 'extern_type': extern_type,
281 281 }
282 282
283 283 # set an admin if we're in admin_groups of crowd
284 284 for group in settings["admin_groups"]:
285 285 if group in user_attrs["groups"]:
286 286 user_attrs["admin"] = True
287 287 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
288 288 log.info('user `%s` authenticated correctly', user_attrs['username'])
289 289 return user_attrs
290
291
292 def includeme(config):
293 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('crowd')
294 plugin_factory(plugin_id).includeme(config)
@@ -1,225 +1,230 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import logging
23 23
24 24 from rhodecode.translation import _
25 25 from rhodecode.authentication.base import (
26 26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 29 from rhodecode.lib.colander_utils import strip_whitespace
30 30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 31 from rhodecode.model.db import User
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 """
39 39 Factory function that is called during plugin discovery.
40 40 It returns the plugin instance.
41 41 """
42 42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 43 return plugin
44 44
45 45
46 46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 47 pass
48 48
49 49
50 50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 51 header = colander.SchemaNode(
52 52 colander.String(),
53 53 default='REMOTE_USER',
54 54 description=_('Header to extract the user from'),
55 55 preparer=strip_whitespace,
56 56 title=_('Header'),
57 57 widget='string')
58 58 fallback_header = colander.SchemaNode(
59 59 colander.String(),
60 60 default='HTTP_X_FORWARDED_USER',
61 61 description=_('Header to extract the user from when main one fails'),
62 62 preparer=strip_whitespace,
63 63 title=_('Fallback header'),
64 64 widget='string')
65 65 clean_username = colander.SchemaNode(
66 66 colander.Boolean(),
67 67 default=True,
68 68 description=_('Perform cleaning of user, if passed user has @ in '
69 69 'username then first part before @ is taken. '
70 70 'If there\'s \\ in the username only the part after '
71 71 ' \\ is taken'),
72 72 missing=False,
73 73 title=_('Clean username'),
74 74 widget='bool')
75 75
76 76
77 77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 78
79 79 def includeme(self, config):
80 80 config.add_authn_plugin(self)
81 81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 82 config.add_view(
83 83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 84 attr='settings_get',
85 85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 86 request_method='GET',
87 87 route_name='auth_home',
88 88 context=HeadersAuthnResource)
89 89 config.add_view(
90 90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 91 attr='settings_post',
92 92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 93 request_method='POST',
94 94 route_name='auth_home',
95 95 context=HeadersAuthnResource)
96 96
97 97 def get_display_name(self):
98 98 return _('Headers')
99 99
100 100 def get_settings_schema(self):
101 101 return HeadersSettingsSchema()
102 102
103 103 @hybrid_property
104 104 def name(self):
105 105 return 'headers'
106 106
107 107 @property
108 108 def is_headers_auth(self):
109 109 return True
110 110
111 111 def use_fake_password(self):
112 112 return True
113 113
114 114 def user_activation_state(self):
115 115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 116 return 'hg.extern_activate.auto' in def_user_perms
117 117
118 118 def _clean_username(self, username):
119 119 # Removing realm and domain from username
120 120 username = username.split('@')[0]
121 121 username = username.rsplit('\\')[-1]
122 122 return username
123 123
124 124 def _get_username(self, environ, settings):
125 125 username = None
126 126 environ = environ or {}
127 127 if not environ:
128 128 log.debug('got empty environ: %s', environ)
129 129
130 130 settings = settings or {}
131 131 if settings.get('header'):
132 132 header = settings.get('header')
133 133 username = environ.get(header)
134 134 log.debug('extracted %s:%s', header, username)
135 135
136 136 # fallback mode
137 137 if not username and settings.get('fallback_header'):
138 138 header = settings.get('fallback_header')
139 139 username = environ.get(header)
140 140 log.debug('extracted %s:%s', header, username)
141 141
142 142 if username and str2bool(settings.get('clean_username')):
143 143 log.debug('Received username `%s` from headers', username)
144 144 username = self._clean_username(username)
145 145 log.debug('New cleanup user is:%s', username)
146 146 return username
147 147
148 148 def get_user(self, username=None, **kwargs):
149 149 """
150 150 Helper method for user fetching in plugins, by default it's using
151 151 simple fetch by username, but this method can be custimized in plugins
152 152 eg. headers auth plugin to fetch user by environ params
153 153 :param username: username if given to fetch
154 154 :param kwargs: extra arguments needed for user fetching.
155 155 """
156 156 environ = kwargs.get('environ') or {}
157 157 settings = kwargs.get('settings') or {}
158 158 username = self._get_username(environ, settings)
159 159 # we got the username, so use default method now
160 160 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 161
162 162 def auth(self, userobj, username, password, settings, **kwargs):
163 163 """
164 164 Get's the headers_auth username (or email). It tries to get username
165 165 from REMOTE_USER if this plugin is enabled, if that fails
166 166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 167 is set. clean_username extracts the username from this data if it's
168 168 having @ in it.
169 169 Return None on failure. On success, return a dictionary of the form:
170 170
171 171 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 172
173 173 :param userobj:
174 174 :param username:
175 175 :param password:
176 176 :param settings:
177 177 :param kwargs:
178 178 """
179 179 environ = kwargs.get('environ')
180 180 if not environ:
181 181 log.debug('Empty environ data skipping...')
182 182 return None
183 183
184 184 if not userobj:
185 185 userobj = self.get_user('', environ=environ, settings=settings)
186 186
187 187 # we don't care passed username/password for headers auth plugins.
188 188 # only way to log in is using environ
189 189 username = None
190 190 if userobj:
191 191 username = getattr(userobj, 'username')
192 192
193 193 if not username:
194 194 # we don't have any objects in DB user doesn't exist extract
195 195 # username from environ based on the settings
196 196 username = self._get_username(environ, settings)
197 197
198 198 # if cannot fetch username, it's a no-go for this plugin to proceed
199 199 if not username:
200 200 return None
201 201
202 202 # old attrs fetched from RhodeCode database
203 203 admin = getattr(userobj, 'admin', False)
204 204 active = getattr(userobj, 'active', True)
205 205 email = getattr(userobj, 'email', '')
206 206 firstname = getattr(userobj, 'firstname', '')
207 207 lastname = getattr(userobj, 'lastname', '')
208 208 extern_type = getattr(userobj, 'extern_type', '')
209 209
210 210 user_attrs = {
211 211 'username': username,
212 212 'firstname': safe_unicode(firstname or username),
213 213 'lastname': safe_unicode(lastname or ''),
214 214 'groups': [],
215 215 'user_group_sync': False,
216 216 'email': email or '',
217 217 'admin': admin or False,
218 218 'active': active,
219 219 'active_from_extern': True,
220 220 'extern_name': username,
221 221 'extern_type': extern_type,
222 222 }
223 223
224 224 log.info('user `%s` authenticated correctly', user_attrs['username'])
225 225 return user_attrs
226
227
228 def includeme(config):
229 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('headers')
230 plugin_factory(plugin_id).includeme(config)
@@ -1,167 +1,172 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for Jasig CAS
23 23 http://www.jasig.org/cas
24 24 """
25 25
26 26
27 27 import colander
28 28 import logging
29 29 import rhodecode
30 30 import urllib
31 31 import urllib2
32 32
33 33 from rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40 from rhodecode.model.db import User
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def plugin_factory(plugin_id, *args, **kwds):
46 46 """
47 47 Factory function that is called during plugin discovery.
48 48 It returns the plugin instance.
49 49 """
50 50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 51 return plugin
52 52
53 53
54 54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 55 pass
56 56
57 57
58 58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 59 service_url = colander.SchemaNode(
60 60 colander.String(),
61 61 default='https://domain.com/cas/v1/tickets',
62 62 description=_('The url of the Jasig CAS REST service'),
63 63 preparer=strip_whitespace,
64 64 title=_('URL'),
65 65 widget='string')
66 66
67 67
68 68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 69
70 70 def includeme(self, config):
71 71 config.add_authn_plugin(self)
72 72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 73 config.add_view(
74 74 'rhodecode.authentication.views.AuthnPluginViewBase',
75 75 attr='settings_get',
76 76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 77 request_method='GET',
78 78 route_name='auth_home',
79 79 context=JasigCasAuthnResource)
80 80 config.add_view(
81 81 'rhodecode.authentication.views.AuthnPluginViewBase',
82 82 attr='settings_post',
83 83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 84 request_method='POST',
85 85 route_name='auth_home',
86 86 context=JasigCasAuthnResource)
87 87
88 88 def get_settings_schema(self):
89 89 return JasigCasSettingsSchema()
90 90
91 91 def get_display_name(self):
92 92 return _('Jasig-CAS')
93 93
94 94 @hybrid_property
95 95 def name(self):
96 96 return "jasig-cas"
97 97
98 98 @property
99 99 def is_headers_auth(self):
100 100 return True
101 101
102 102 def use_fake_password(self):
103 103 return True
104 104
105 105 def user_activation_state(self):
106 106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 107 return 'hg.extern_activate.auto' in def_user_perms
108 108
109 109 def auth(self, userobj, username, password, settings, **kwargs):
110 110 """
111 111 Given a user object (which may be null), username, a plaintext password,
112 112 and a settings object (containing all the keys needed as listed in settings()),
113 113 authenticate this user's login attempt.
114 114
115 115 Return None on failure. On success, return a dictionary of the form:
116 116
117 117 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 118 This is later validated for correctness
119 119 """
120 120 if not username or not password:
121 121 log.debug('Empty username or password skipping...')
122 122 return None
123 123
124 124 log.debug("Jasig CAS settings: %s", settings)
125 125 params = urllib.urlencode({'username': username, 'password': password})
126 126 headers = {"Content-type": "application/x-www-form-urlencoded",
127 127 "Accept": "text/plain",
128 128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 129 url = settings["service_url"]
130 130
131 131 log.debug("Sent Jasig CAS: \n%s",
132 132 {"url": url, "body": params, "headers": headers})
133 133 request = urllib2.Request(url, params, headers)
134 134 try:
135 135 response = urllib2.urlopen(request)
136 136 except urllib2.HTTPError as e:
137 137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
138 138 return None
139 139 except urllib2.URLError as e:
140 140 log.debug("URLError when requesting Jasig CAS url: %s ", url)
141 141 return None
142 142
143 143 # old attrs fetched from RhodeCode database
144 144 admin = getattr(userobj, 'admin', False)
145 145 active = getattr(userobj, 'active', True)
146 146 email = getattr(userobj, 'email', '')
147 147 username = getattr(userobj, 'username', username)
148 148 firstname = getattr(userobj, 'firstname', '')
149 149 lastname = getattr(userobj, 'lastname', '')
150 150 extern_type = getattr(userobj, 'extern_type', '')
151 151
152 152 user_attrs = {
153 153 'username': username,
154 154 'firstname': safe_unicode(firstname or username),
155 155 'lastname': safe_unicode(lastname or ''),
156 156 'groups': [],
157 157 'user_group_sync': False,
158 158 'email': email or '',
159 159 'admin': admin or False,
160 160 'active': active,
161 161 'active_from_extern': True,
162 162 'extern_name': username,
163 163 'extern_type': extern_type,
164 164 }
165 165
166 166 log.info('user `%s` authenticated correctly', user_attrs['username'])
167 167 return user_attrs
168
169
170 def includeme(config):
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('jasig_cas')
172 plugin_factory(plugin_id).includeme(config)
@@ -1,528 +1,533 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for LDAP
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import colander
29 29 from rhodecode.translation import _
30 30 from rhodecode.authentication.base import (
31 31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 34 from rhodecode.lib.colander_utils import strip_whitespace
35 35 from rhodecode.lib.exceptions import (
36 36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 37 )
38 38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 39 from rhodecode.model.db import User
40 40 from rhodecode.model.validators import Missing
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 try:
45 45 import ldap
46 46 except ImportError:
47 47 # means that python-ldap is not installed, we use Missing object to mark
48 48 # ldap lib is Missing
49 49 ldap = Missing
50 50
51 51
52 52 class LdapError(Exception):
53 53 pass
54 54
55 55
56 56 def plugin_factory(plugin_id, *args, **kwds):
57 57 """
58 58 Factory function that is called during plugin discovery.
59 59 It returns the plugin instance.
60 60 """
61 61 plugin = RhodeCodeAuthPlugin(plugin_id)
62 62 return plugin
63 63
64 64
65 65 class LdapAuthnResource(AuthnPluginResourceBase):
66 66 pass
67 67
68 68
69 69 class AuthLdap(AuthLdapBase):
70 70 default_tls_cert_dir = '/etc/openldap/cacerts'
71 71
72 72 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
73 73 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
74 74 tls_cert_dir=None, ldap_version=3,
75 75 search_scope='SUBTREE', attr_login='uid',
76 76 ldap_filter='', timeout=None):
77 77 if ldap == Missing:
78 78 raise LdapImportError("Missing or incompatible ldap library")
79 79
80 80 self.debug = False
81 81 self.timeout = timeout or 60 * 5
82 82 self.ldap_version = ldap_version
83 83 self.ldap_server_type = 'ldap'
84 84
85 85 self.TLS_KIND = tls_kind
86 86
87 87 if self.TLS_KIND == 'LDAPS':
88 88 port = port or 689
89 89 self.ldap_server_type += 's'
90 90
91 91 OPT_X_TLS_DEMAND = 2
92 92 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
93 93 self.TLS_CERT_FILE = tls_cert_file or ''
94 94 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
95 95
96 96 # split server into list
97 97 self.SERVER_ADDRESSES = self._get_server_list(server)
98 98 self.LDAP_SERVER_PORT = port
99 99
100 100 # USE FOR READ ONLY BIND TO LDAP SERVER
101 101 self.attr_login = attr_login
102 102
103 103 self.LDAP_BIND_DN = safe_str(bind_dn)
104 104 self.LDAP_BIND_PASS = safe_str(bind_pass)
105 105
106 106 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
107 107 self.BASE_DN = safe_str(base_dn)
108 108 self.LDAP_FILTER = safe_str(ldap_filter)
109 109
110 110 def _get_ldap_conn(self):
111 111
112 112 if self.debug:
113 113 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
114 114
115 115 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
116 116 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
117 117
118 118 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
119 119 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
120 120
121 121 if self.TLS_KIND != 'PLAIN':
122 122 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
123 123
124 124 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
125 125 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
126 126
127 127 # init connection now
128 128 ldap_servers = self._build_servers(
129 129 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
130 130 log.debug('initializing LDAP connection to:%s', ldap_servers)
131 131 ldap_conn = ldap.initialize(ldap_servers)
132 132 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
133 133 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
134 134 ldap_conn.timeout = self.timeout
135 135
136 136 if self.ldap_version == 2:
137 137 ldap_conn.protocol = ldap.VERSION2
138 138 else:
139 139 ldap_conn.protocol = ldap.VERSION3
140 140
141 141 if self.TLS_KIND == 'START_TLS':
142 142 ldap_conn.start_tls_s()
143 143
144 144 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
145 145 log.debug('Trying simple_bind with password and given login DN: %s',
146 146 self.LDAP_BIND_DN)
147 147 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
148 148
149 149 return ldap_conn
150 150
151 151 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
152 152 try:
153 153 log.debug('Trying simple bind with %s', dn)
154 154 server.simple_bind_s(dn, safe_str(password))
155 155 user = server.search_ext_s(
156 156 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
157 157 _, attrs = user
158 158 return attrs
159 159
160 160 except ldap.INVALID_CREDENTIALS:
161 161 log.debug(
162 162 "LDAP rejected password for user '%s': %s, org_exc:",
163 163 username, dn, exc_info=True)
164 164
165 165 def authenticate_ldap(self, username, password):
166 166 """
167 167 Authenticate a user via LDAP and return his/her LDAP properties.
168 168
169 169 Raises AuthenticationError if the credentials are rejected, or
170 170 EnvironmentError if the LDAP server can't be reached.
171 171
172 172 :param username: username
173 173 :param password: password
174 174 """
175 175
176 176 uid = self.get_uid(username, self.SERVER_ADDRESSES)
177 177 user_attrs = {}
178 178 dn = ''
179 179
180 180 self.validate_password(username, password)
181 181 self.validate_username(username)
182 182
183 183 ldap_conn = None
184 184 try:
185 185 ldap_conn = self._get_ldap_conn()
186 186 filter_ = '(&%s(%s=%s))' % (
187 187 self.LDAP_FILTER, self.attr_login, username)
188 188 log.debug("Authenticating %r filter %s", self.BASE_DN, filter_)
189 189
190 190 lobjects = ldap_conn.search_ext_s(
191 191 self.BASE_DN, self.SEARCH_SCOPE, filter_)
192 192
193 193 if not lobjects:
194 194 log.debug("No matching LDAP objects for authentication "
195 195 "of UID:'%s' username:(%s)", uid, username)
196 196 raise ldap.NO_SUCH_OBJECT()
197 197
198 198 log.debug('Found matching ldap object, trying to authenticate')
199 199 for (dn, _attrs) in lobjects:
200 200 if dn is None:
201 201 continue
202 202
203 203 user_attrs = self.fetch_attrs_from_simple_bind(
204 204 ldap_conn, dn, username, password)
205 205 if user_attrs:
206 206 break
207 207 else:
208 208 raise LdapPasswordError(
209 209 'Failed to authenticate user `{}`'
210 210 'with given password'.format(username))
211 211
212 212 except ldap.NO_SUCH_OBJECT:
213 213 log.debug("LDAP says no such user '%s' (%s), org_exc:",
214 214 uid, username, exc_info=True)
215 215 raise LdapUsernameError('Unable to find user')
216 216 except ldap.SERVER_DOWN:
217 217 org_exc = traceback.format_exc()
218 218 raise LdapConnectionError(
219 219 "LDAP can't access authentication "
220 220 "server, org_exc:%s" % org_exc)
221 221 finally:
222 222 if ldap_conn:
223 223 log.debug('ldap: connection release')
224 224 try:
225 225 ldap_conn.unbind_s()
226 226 except Exception:
227 227 # for any reason this can raise exception we must catch it
228 228 # to not crush the server
229 229 pass
230 230
231 231 return dn, user_attrs
232 232
233 233
234 234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
235 235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
236 236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
237 237 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
238 238
239 239 host = colander.SchemaNode(
240 240 colander.String(),
241 241 default='',
242 242 description=_('Host[s] of the LDAP Server \n'
243 243 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
244 244 'Multiple servers can be specified using commas'),
245 245 preparer=strip_whitespace,
246 246 title=_('LDAP Host'),
247 247 widget='string')
248 248 port = colander.SchemaNode(
249 249 colander.Int(),
250 250 default=389,
251 251 description=_('Custom port that the LDAP server is listening on. '
252 252 'Default value is: 389'),
253 253 preparer=strip_whitespace,
254 254 title=_('Port'),
255 255 validator=colander.Range(min=0, max=65536),
256 256 widget='int')
257 257
258 258 timeout = colander.SchemaNode(
259 259 colander.Int(),
260 260 default=60 * 5,
261 261 description=_('Timeout for LDAP connection'),
262 262 preparer=strip_whitespace,
263 263 title=_('Connection timeout'),
264 264 validator=colander.Range(min=1),
265 265 widget='int')
266 266
267 267 dn_user = colander.SchemaNode(
268 268 colander.String(),
269 269 default='',
270 270 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
271 271 'e.g., cn=admin,dc=mydomain,dc=com, or '
272 272 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
273 273 missing='',
274 274 preparer=strip_whitespace,
275 275 title=_('Account'),
276 276 widget='string')
277 277 dn_pass = colander.SchemaNode(
278 278 colander.String(),
279 279 default='',
280 280 description=_('Password to authenticate for given user DN.'),
281 281 missing='',
282 282 preparer=strip_whitespace,
283 283 title=_('Password'),
284 284 widget='password')
285 285 tls_kind = colander.SchemaNode(
286 286 colander.String(),
287 287 default=tls_kind_choices[0],
288 288 description=_('TLS Type'),
289 289 title=_('Connection Security'),
290 290 validator=colander.OneOf(tls_kind_choices),
291 291 widget='select')
292 292 tls_reqcert = colander.SchemaNode(
293 293 colander.String(),
294 294 default=tls_reqcert_choices[0],
295 295 description=_('Require Cert over TLS?. Self-signed and custom '
296 296 'certificates can be used when\n `RhodeCode Certificate` '
297 297 'found in admin > settings > system info page is extended.'),
298 298 title=_('Certificate Checks'),
299 299 validator=colander.OneOf(tls_reqcert_choices),
300 300 widget='select')
301 301 tls_cert_file = colander.SchemaNode(
302 302 colander.String(),
303 303 default='',
304 304 description=_('This specifies the PEM-format file path containing '
305 305 'certificates for use in TLS connection.\n'
306 306 'If not specified `TLS Cert dir` will be used'),
307 307 title=_('TLS Cert file'),
308 308 missing='',
309 309 widget='string')
310 310 tls_cert_dir = colander.SchemaNode(
311 311 colander.String(),
312 312 default=AuthLdap.default_tls_cert_dir,
313 313 description=_('This specifies the path of a directory that contains individual '
314 314 'CA certificates in separate files.'),
315 315 title=_('TLS Cert dir'),
316 316 widget='string')
317 317 base_dn = colander.SchemaNode(
318 318 colander.String(),
319 319 default='',
320 320 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
321 321 'in it to be replaced with current user credentials \n'
322 322 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
323 323 missing='',
324 324 preparer=strip_whitespace,
325 325 title=_('Base DN'),
326 326 widget='string')
327 327 filter = colander.SchemaNode(
328 328 colander.String(),
329 329 default='',
330 330 description=_('Filter to narrow results \n'
331 331 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
332 332 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
333 333 missing='',
334 334 preparer=strip_whitespace,
335 335 title=_('LDAP Search Filter'),
336 336 widget='string')
337 337
338 338 search_scope = colander.SchemaNode(
339 339 colander.String(),
340 340 default=search_scope_choices[2],
341 341 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
342 342 title=_('LDAP Search Scope'),
343 343 validator=colander.OneOf(search_scope_choices),
344 344 widget='select')
345 345 attr_login = colander.SchemaNode(
346 346 colander.String(),
347 347 default='uid',
348 348 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
349 349 preparer=strip_whitespace,
350 350 title=_('Login Attribute'),
351 351 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
352 352 widget='string')
353 353 attr_firstname = colander.SchemaNode(
354 354 colander.String(),
355 355 default='',
356 356 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
357 357 missing='',
358 358 preparer=strip_whitespace,
359 359 title=_('First Name Attribute'),
360 360 widget='string')
361 361 attr_lastname = colander.SchemaNode(
362 362 colander.String(),
363 363 default='',
364 364 description=_('LDAP Attribute to map to last name (e.g., sn)'),
365 365 missing='',
366 366 preparer=strip_whitespace,
367 367 title=_('Last Name Attribute'),
368 368 widget='string')
369 369 attr_email = colander.SchemaNode(
370 370 colander.String(),
371 371 default='',
372 372 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
373 373 'Emails are a crucial part of RhodeCode. \n'
374 374 'If possible add a valid email attribute to ldap users.'),
375 375 missing='',
376 376 preparer=strip_whitespace,
377 377 title=_('Email Attribute'),
378 378 widget='string')
379 379
380 380
381 381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
382 382 # used to define dynamic binding in the
383 383 DYNAMIC_BIND_VAR = '$login'
384 384 _settings_unsafe_keys = ['dn_pass']
385 385
386 386 def includeme(self, config):
387 387 config.add_authn_plugin(self)
388 388 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
389 389 config.add_view(
390 390 'rhodecode.authentication.views.AuthnPluginViewBase',
391 391 attr='settings_get',
392 392 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
393 393 request_method='GET',
394 394 route_name='auth_home',
395 395 context=LdapAuthnResource)
396 396 config.add_view(
397 397 'rhodecode.authentication.views.AuthnPluginViewBase',
398 398 attr='settings_post',
399 399 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
400 400 request_method='POST',
401 401 route_name='auth_home',
402 402 context=LdapAuthnResource)
403 403
404 404 def get_settings_schema(self):
405 405 return LdapSettingsSchema()
406 406
407 407 def get_display_name(self):
408 408 return _('LDAP')
409 409
410 410 @classmethod
411 411 def docs(cls):
412 412 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
413 413
414 414 @hybrid_property
415 415 def name(self):
416 416 return "ldap"
417 417
418 418 def use_fake_password(self):
419 419 return True
420 420
421 421 def user_activation_state(self):
422 422 def_user_perms = User.get_default_user().AuthUser().permissions['global']
423 423 return 'hg.extern_activate.auto' in def_user_perms
424 424
425 425 def try_dynamic_binding(self, username, password, current_args):
426 426 """
427 427 Detects marker inside our original bind, and uses dynamic auth if
428 428 present
429 429 """
430 430
431 431 org_bind = current_args['bind_dn']
432 432 passwd = current_args['bind_pass']
433 433
434 434 def has_bind_marker(username):
435 435 if self.DYNAMIC_BIND_VAR in username:
436 436 return True
437 437
438 438 # we only passed in user with "special" variable
439 439 if org_bind and has_bind_marker(org_bind) and not passwd:
440 440 log.debug('Using dynamic user/password binding for ldap '
441 441 'authentication. Replacing `%s` with username',
442 442 self.DYNAMIC_BIND_VAR)
443 443 current_args['bind_dn'] = org_bind.replace(
444 444 self.DYNAMIC_BIND_VAR, username)
445 445 current_args['bind_pass'] = password
446 446
447 447 return current_args
448 448
449 449 def auth(self, userobj, username, password, settings, **kwargs):
450 450 """
451 451 Given a user object (which may be null), username, a plaintext password,
452 452 and a settings object (containing all the keys needed as listed in
453 453 settings()), authenticate this user's login attempt.
454 454
455 455 Return None on failure. On success, return a dictionary of the form:
456 456
457 457 see: RhodeCodeAuthPluginBase.auth_func_attrs
458 458 This is later validated for correctness
459 459 """
460 460
461 461 if not username or not password:
462 462 log.debug('Empty username or password skipping...')
463 463 return None
464 464
465 465 ldap_args = {
466 466 'server': settings.get('host', ''),
467 467 'base_dn': settings.get('base_dn', ''),
468 468 'port': settings.get('port'),
469 469 'bind_dn': settings.get('dn_user'),
470 470 'bind_pass': settings.get('dn_pass'),
471 471 'tls_kind': settings.get('tls_kind'),
472 472 'tls_reqcert': settings.get('tls_reqcert'),
473 473 'tls_cert_file': settings.get('tls_cert_file'),
474 474 'tls_cert_dir': settings.get('tls_cert_dir'),
475 475 'search_scope': settings.get('search_scope'),
476 476 'attr_login': settings.get('attr_login'),
477 477 'ldap_version': 3,
478 478 'ldap_filter': settings.get('filter'),
479 479 'timeout': settings.get('timeout')
480 480 }
481 481
482 482 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
483 483
484 484 log.debug('Checking for ldap authentication.')
485 485
486 486 try:
487 487 aldap = AuthLdap(**ldap_args)
488 488 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
489 489 log.debug('Got ldap DN response %s', user_dn)
490 490
491 491 def get_ldap_attr(k):
492 492 return ldap_attrs.get(settings.get(k), [''])[0]
493 493
494 494 # old attrs fetched from RhodeCode database
495 495 admin = getattr(userobj, 'admin', False)
496 496 active = getattr(userobj, 'active', True)
497 497 email = getattr(userobj, 'email', '')
498 498 username = getattr(userobj, 'username', username)
499 499 firstname = getattr(userobj, 'firstname', '')
500 500 lastname = getattr(userobj, 'lastname', '')
501 501 extern_type = getattr(userobj, 'extern_type', '')
502 502
503 503 groups = []
504 504 user_attrs = {
505 505 'username': username,
506 506 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
507 507 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
508 508 'groups': groups,
509 509 'user_group_sync': False,
510 510 'email': get_ldap_attr('attr_email') or email,
511 511 'admin': admin,
512 512 'active': active,
513 513 'active_from_extern': None,
514 514 'extern_name': user_dn,
515 515 'extern_type': extern_type,
516 516 }
517 517
518 518 log.debug('ldap user: %s', user_attrs)
519 519 log.info('user `%s` authenticated correctly', user_attrs['username'])
520 520
521 521 return user_attrs
522 522
523 523 except (LdapUsernameError, LdapPasswordError, LdapImportError):
524 524 log.exception("LDAP related exception")
525 525 return None
526 526 except (Exception,):
527 527 log.exception("Other exception")
528 528 return None
529
530
531 def includeme(config):
532 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('ldap')
533 plugin_factory(plugin_id).includeme(config)
@@ -1,165 +1,170 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication library for PAM
23 23 """
24 24
25 25 import colander
26 26 import grp
27 27 import logging
28 28 import pam
29 29 import pwd
30 30 import re
31 31 import socket
32 32
33 33 from rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwds):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class PamAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 service = colander.SchemaNode(
58 58 colander.String(),
59 59 default='login',
60 60 description=_('PAM service name to use for authentication.'),
61 61 preparer=strip_whitespace,
62 62 title=_('PAM service name'),
63 63 widget='string')
64 64 gecos = colander.SchemaNode(
65 65 colander.String(),
66 66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 67 description=_('Regular expression for extracting user name/email etc. '
68 68 'from Unix userinfo.'),
69 69 preparer=strip_whitespace,
70 70 title=_('Gecos Regex'),
71 71 widget='string')
72 72
73 73
74 74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 75 # PAM authentication can be slow. Repository operations involve a lot of
76 76 # auth calls. Little caching helps speedup push/pull operations significantly
77 77 AUTH_CACHE_TTL = 4
78 78
79 79 def includeme(self, config):
80 80 config.add_authn_plugin(self)
81 81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 82 config.add_view(
83 83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 84 attr='settings_get',
85 85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 86 request_method='GET',
87 87 route_name='auth_home',
88 88 context=PamAuthnResource)
89 89 config.add_view(
90 90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 91 attr='settings_post',
92 92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 93 request_method='POST',
94 94 route_name='auth_home',
95 95 context=PamAuthnResource)
96 96
97 97 def get_display_name(self):
98 98 return _('PAM')
99 99
100 100 @classmethod
101 101 def docs(cls):
102 102 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
103 103
104 104 @hybrid_property
105 105 def name(self):
106 106 return "pam"
107 107
108 108 def get_settings_schema(self):
109 109 return PamSettingsSchema()
110 110
111 111 def use_fake_password(self):
112 112 return True
113 113
114 114 def auth(self, userobj, username, password, settings, **kwargs):
115 115 if not username or not password:
116 116 log.debug('Empty username or password skipping...')
117 117 return None
118 118 _pam = pam.pam()
119 119 auth_result = _pam.authenticate(username, password, settings["service"])
120 120
121 121 if not auth_result:
122 122 log.error("PAM was unable to authenticate user: %s", username)
123 123 return None
124 124
125 125 log.debug('Got PAM response %s', auth_result)
126 126
127 127 # old attrs fetched from RhodeCode database
128 128 default_email = "%s@%s" % (username, socket.gethostname())
129 129 admin = getattr(userobj, 'admin', False)
130 130 active = getattr(userobj, 'active', True)
131 131 email = getattr(userobj, 'email', '') or default_email
132 132 username = getattr(userobj, 'username', username)
133 133 firstname = getattr(userobj, 'firstname', '')
134 134 lastname = getattr(userobj, 'lastname', '')
135 135 extern_type = getattr(userobj, 'extern_type', '')
136 136
137 137 user_attrs = {
138 138 'username': username,
139 139 'firstname': firstname,
140 140 'lastname': lastname,
141 141 'groups': [g.gr_name for g in grp.getgrall()
142 142 if username in g.gr_mem],
143 143 'user_group_sync': True,
144 144 'email': email,
145 145 'admin': admin,
146 146 'active': active,
147 147 'active_from_extern': None,
148 148 'extern_name': username,
149 149 'extern_type': extern_type,
150 150 }
151 151
152 152 try:
153 153 user_data = pwd.getpwnam(username)
154 154 regex = settings["gecos"]
155 155 match = re.search(regex, user_data.pw_gecos)
156 156 if match:
157 157 user_attrs["firstname"] = match.group('first_name')
158 158 user_attrs["lastname"] = match.group('last_name')
159 159 except Exception:
160 160 log.warning("Cannot extract additional info for PAM user")
161 161 pass
162 162
163 163 log.debug("pamuser: %s", user_attrs)
164 164 log.info('user `%s` authenticated correctly', user_attrs['username'])
165 165 return user_attrs
166
167
168 def includeme(config):
169 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('pam')
170 plugin_factory(plugin_id).includeme(config)
@@ -1,143 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from rhodecode.translation import _
28 28
29 29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 31 from rhodecode.lib.utils2 import safe_str
32 32 from rhodecode.model.db import User
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 39 return plugin
40 40
41 41
42 42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 43 pass
44 44
45 45
46 46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 47
48 48 def includeme(self, config):
49 49 config.add_authn_plugin(self)
50 50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 51 config.add_view(
52 52 'rhodecode.authentication.views.AuthnPluginViewBase',
53 53 attr='settings_get',
54 54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 55 request_method='GET',
56 56 route_name='auth_home',
57 57 context=RhodecodeAuthnResource)
58 58 config.add_view(
59 59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 60 attr='settings_post',
61 61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 62 request_method='POST',
63 63 route_name='auth_home',
64 64 context=RhodecodeAuthnResource)
65 65
66 66 def get_display_name(self):
67 67 return _('RhodeCode Internal')
68 68
69 69 @hybrid_property
70 70 def name(self):
71 71 return "rhodecode"
72 72
73 73 def user_activation_state(self):
74 74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
75 75 return 'hg.register.auto_activate' in def_user_perms
76 76
77 77 def allows_authentication_from(
78 78 self, user, allows_non_existing_user=True,
79 79 allowed_auth_plugins=None, allowed_auth_sources=None):
80 80 """
81 81 Custom method for this auth that doesn't accept non existing users.
82 82 We know that user exists in our database.
83 83 """
84 84 allows_non_existing_user = False
85 85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 86 user, allows_non_existing_user=allows_non_existing_user)
87 87
88 88 def auth(self, userobj, username, password, settings, **kwargs):
89 89 if not userobj:
90 90 log.debug('userobj was:%s skipping', userobj)
91 91 return None
92 92 if userobj.extern_type != self.name:
93 93 log.warning(
94 94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`",
95 95 userobj, userobj.extern_type, self.name)
96 96 return None
97 97
98 98 user_attrs = {
99 99 "username": userobj.username,
100 100 "firstname": userobj.firstname,
101 101 "lastname": userobj.lastname,
102 102 "groups": [],
103 103 'user_group_sync': False,
104 104 "email": userobj.email,
105 105 "admin": userobj.admin,
106 106 "active": userobj.active,
107 107 "active_from_extern": userobj.active,
108 108 "extern_name": userobj.user_id,
109 109 "extern_type": userobj.extern_type,
110 110 }
111 111
112 112 log.debug("User attributes:%s", user_attrs)
113 113 if userobj.active:
114 114 from rhodecode.lib import auth
115 115 crypto_backend = auth.crypto_backend()
116 116 password_encoded = safe_str(password)
117 117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
118 118 password_encoded, userobj.password or '')
119 119
120 120 if password_match and new_hash:
121 121 log.debug('user %s properly authenticated, but '
122 122 'requires hash change to bcrypt', userobj)
123 123 # if password match, and we use OLD deprecated hash,
124 124 # we should migrate this user hash password to the new hash
125 125 # we store the new returned by hash_check_with_upgrade function
126 126 user_attrs['_hash_migrate'] = new_hash
127 127
128 128 if userobj.username == User.DEFAULT_USER and userobj.active:
129 129 log.info(
130 130 'user `%s` authenticated correctly as anonymous user', userobj.username)
131 131 return user_attrs
132 132
133 133 elif userobj.username == username and password_match:
134 134 log.info('user `%s` authenticated correctly', userobj.username)
135 135 return user_attrs
136 136 log.warn("user `%s` used a wrong password when "
137 137 "authenticating on this plugin", userobj.username)
138 138 return None
139 139 else:
140 140 log.warning(
141 141 'user `%s` failed to authenticate via %s, reason: account not '
142 142 'active.', username, self.name)
143 143 return None
144
145
146 def includeme(config):
147 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('rhodecode')
148 plugin_factory(plugin_id).includeme(config)
@@ -1,151 +1,156 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication token plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from rhodecode.translation import _
28 28 from rhodecode.authentication.base import (
29 29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 31 from rhodecode.model.db import User, UserApiKeys, Repository
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 39 return plugin
40 40
41 41
42 42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 43 pass
44 44
45 45
46 46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 47 """
48 48 Enables usage of authentication tokens for vcs operations.
49 49 """
50 50
51 51 def includeme(self, config):
52 52 config.add_authn_plugin(self)
53 53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 54 config.add_view(
55 55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 56 attr='settings_get',
57 57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 58 request_method='GET',
59 59 route_name='auth_home',
60 60 context=RhodecodeAuthnResource)
61 61 config.add_view(
62 62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 63 attr='settings_post',
64 64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 65 request_method='POST',
66 66 route_name='auth_home',
67 67 context=RhodecodeAuthnResource)
68 68
69 69 def get_display_name(self):
70 70 return _('Rhodecode Token')
71 71
72 72 @classmethod
73 73 def docs(cls):
74 74 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
75 75
76 76 @hybrid_property
77 77 def name(self):
78 78 return "authtoken"
79 79
80 80 def user_activation_state(self):
81 81 def_user_perms = User.get_default_user().AuthUser().permissions['global']
82 82 return 'hg.register.auto_activate' in def_user_perms
83 83
84 84 def allows_authentication_from(
85 85 self, user, allows_non_existing_user=True,
86 86 allowed_auth_plugins=None, allowed_auth_sources=None):
87 87 """
88 88 Custom method for this auth that doesn't accept empty users. And also
89 89 allows users from all other active plugins to use it and also
90 90 authenticate against it. But only via vcs mode
91 91 """
92 92 from rhodecode.authentication.base import get_authn_registry
93 93 authn_registry = get_authn_registry()
94 94
95 95 active_plugins = set(
96 96 [x.name for x in authn_registry.get_plugins_for_authentication()])
97 97 active_plugins.discard(self.name)
98 98
99 99 allowed_auth_plugins = [self.name] + list(active_plugins)
100 100 # only for vcs operations
101 101 allowed_auth_sources = [VCS_TYPE]
102 102
103 103 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
104 104 user, allows_non_existing_user=False,
105 105 allowed_auth_plugins=allowed_auth_plugins,
106 106 allowed_auth_sources=allowed_auth_sources)
107 107
108 108 def auth(self, userobj, username, password, settings, **kwargs):
109 109 if not userobj:
110 110 log.debug('userobj was:%s skipping', userobj)
111 111 return None
112 112
113 113 user_attrs = {
114 114 "username": userobj.username,
115 115 "firstname": userobj.firstname,
116 116 "lastname": userobj.lastname,
117 117 "groups": [],
118 118 'user_group_sync': False,
119 119 "email": userobj.email,
120 120 "admin": userobj.admin,
121 121 "active": userobj.active,
122 122 "active_from_extern": userobj.active,
123 123 "extern_name": userobj.user_id,
124 124 "extern_type": userobj.extern_type,
125 125 }
126 126
127 127 log.debug('Authenticating user with args %s', user_attrs)
128 128 if userobj.active:
129 129 # calling context repo for token scopes
130 130 scope_repo_id = None
131 131 if self.acl_repo_name:
132 132 repo = Repository.get_by_repo_name(self.acl_repo_name)
133 133 scope_repo_id = repo.repo_id if repo else None
134 134
135 135 token_match = userobj.authenticate_by_token(
136 136 password, roles=[UserApiKeys.ROLE_VCS],
137 137 scope_repo_id=scope_repo_id)
138 138
139 139 if userobj.username == username and token_match:
140 140 log.info(
141 141 'user `%s` successfully authenticated via %s',
142 142 user_attrs['username'], self.name)
143 143 return user_attrs
144 144 log.warn(
145 145 'user `%s` failed to authenticate via %s, reason: bad or '
146 146 'inactive token.', username, self.name)
147 147 else:
148 148 log.warning(
149 149 'user `%s` failed to authenticate via %s, reason: account not '
150 150 'active.', username, self.name)
151 151 return None
152
153
154 def includeme(config):
155 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('token')
156 plugin_factory(plugin_id).includeme(config)
@@ -1,588 +1,588 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26 import time
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 import rhodecode.events
44 44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 45 from rhodecode.lib.request import Request
46 46 from rhodecode.lib.vcs import VCSCommunicationError
47 47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 50 from rhodecode.lib.celerylib.loader import configure_celery
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 56 write_metadata_if_needed, inject_app_settings)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 67 def make_pyramid_app(global_config, **settings):
68 68 """
69 69 Constructs the WSGI application based on Pyramid.
70 70
71 71 Specials:
72 72
73 73 * The application can also be integrated like a plugin via the call to
74 74 `includeme`. This is accompanied with the other utility functions which
75 75 are called. Changing this should be done with great care to not break
76 76 cases when these fragments are assembled from another place.
77 77
78 78 """
79 79
80 80 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
81 81 # will be replaced by the value of the environment variable "NAME" in this case.
82 82 start_time = time.time()
83 83
84 84 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
85 85
86 86 global_config = _substitute_values(global_config, environ)
87 87 settings = _substitute_values(settings, environ)
88 88
89 89 sanitize_settings_and_apply_defaults(settings)
90 90
91 91 config = Configurator(settings=settings)
92 92
93 93 # Apply compatibility patches
94 94 patches.inspect_getargspec()
95 95
96 96 load_pyramid_environment(global_config, settings)
97 97
98 98 # Static file view comes first
99 99 includeme_first(config)
100 100
101 101 includeme(config)
102 102
103 103 pyramid_app = config.make_wsgi_app()
104 104 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
105 105 pyramid_app.config = config
106 106
107 107 config.configure_celery(global_config['__file__'])
108 108 # creating the app uses a connection - return it after we are done
109 109 meta.Session.remove()
110 110 total_time = time.time() - start_time
111 log.info('Pyramid app %s created and configured in %.2fs', pyramid_app, total_time)
111 log.info('Pyramid app `%s` created and configured in %.2fs',
112 pyramid_app.func_name, total_time)
112 113 return pyramid_app
113 114
114 115
115 116 def not_found_view(request):
116 117 """
117 118 This creates the view which should be registered as not-found-view to
118 119 pyramid.
119 120 """
120 121
121 122 if not getattr(request, 'vcs_call', None):
122 123 # handle like regular case with our error_handler
123 124 return error_handler(HTTPNotFound(), request)
124 125
125 126 # handle not found view as a vcs call
126 127 settings = request.registry.settings
127 128 ae_client = getattr(request, 'ae_client', None)
128 129 vcs_app = VCSMiddleware(
129 130 HTTPNotFound(), request.registry, settings,
130 131 appenlight_client=ae_client)
131 132
132 133 return wsgiapp(vcs_app)(None, request)
133 134
134 135
135 136 def error_handler(exception, request):
136 137 import rhodecode
137 138 from rhodecode.lib import helpers
138 139
139 140 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
140 141
141 142 base_response = HTTPInternalServerError()
142 143 # prefer original exception for the response since it may have headers set
143 144 if isinstance(exception, HTTPException):
144 145 base_response = exception
145 146 elif isinstance(exception, VCSCommunicationError):
146 147 base_response = VCSServerUnavailable()
147 148
148 149 if is_http_error(base_response):
149 150 log.exception(
150 151 'error occurred handling this request for path: %s', request.path)
151 152
152 153 error_explanation = base_response.explanation or str(base_response)
153 154 if base_response.status_code == 404:
154 155 error_explanation += " Or you don't have permission to access it."
155 156 c = AttributeDict()
156 157 c.error_message = base_response.status
157 158 c.error_explanation = error_explanation
158 159 c.visual = AttributeDict()
159 160
160 161 c.visual.rhodecode_support_url = (
161 162 request.registry.settings.get('rhodecode_support_url') or
162 163 request.route_url('rhodecode_support')
163 164 )
164 165 c.redirect_time = 0
165 166 c.rhodecode_name = rhodecode_title
166 167 if not c.rhodecode_name:
167 168 c.rhodecode_name = 'Rhodecode'
168 169
169 170 c.causes = []
170 171 if is_http_error(base_response):
171 172 c.causes.append('Server is overloaded.')
172 173 c.causes.append('Server database connection is lost.')
173 174 c.causes.append('Server expected unhandled error.')
174 175
175 176 if hasattr(base_response, 'causes'):
176 177 c.causes = base_response.causes
177 178
178 179 c.messages = helpers.flash.pop_messages(request=request)
179 180
180 181 exc_info = sys.exc_info()
181 182 c.exception_id = id(exc_info)
182 183 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
183 184 or base_response.status_code > 499
184 185 c.exception_id_url = request.route_url(
185 186 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
186 187
187 188 if c.show_exception_id:
188 189 store_exception(c.exception_id, exc_info)
189 190
190 191 response = render_to_response(
191 192 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
192 193 response=base_response)
193 194
194 195 return response
195 196
196 197
197 198 def includeme_first(config):
198 199 # redirect automatic browser favicon.ico requests to correct place
199 200 def favicon_redirect(context, request):
200 201 return HTTPFound(
201 202 request.static_path('rhodecode:public/images/favicon.ico'))
202 203
203 204 config.add_view(favicon_redirect, route_name='favicon')
204 205 config.add_route('favicon', '/favicon.ico')
205 206
206 207 def robots_redirect(context, request):
207 208 return HTTPFound(
208 209 request.static_path('rhodecode:public/robots.txt'))
209 210
210 211 config.add_view(robots_redirect, route_name='robots')
211 212 config.add_route('robots', '/robots.txt')
212 213
213 214 config.add_static_view(
214 215 '_static/deform', 'deform:static')
215 216 config.add_static_view(
216 217 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
217 218
218 219
219 220 def includeme(config):
220 221 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
221 222 settings = config.registry.settings
222 223 config.set_request_factory(Request)
223 224
224 225 # plugin information
225 226 config.registry.rhodecode_plugins = collections.OrderedDict()
226 227
227 228 config.add_directive(
228 229 'register_rhodecode_plugin', register_rhodecode_plugin)
229 230
230 231 config.add_directive('configure_celery', configure_celery)
231 232
232 233 if asbool(settings.get('appenlight', 'false')):
233 234 config.include('appenlight_client.ext.pyramid_tween')
234 235
235 236 # Includes which are required. The application would fail without them.
236 237 config.include('pyramid_mako')
237 238 config.include('pyramid_beaker')
238 239 config.include('rhodecode.lib.rc_cache')
239 240
240 config.include('rhodecode.authentication')
241 config.include('rhodecode.integrations')
242
243 241 config.include('rhodecode.apps._base.navigation')
244 242 config.include('rhodecode.apps._base.subscribers')
245 243 config.include('rhodecode.tweens')
246 244
245 config.include('rhodecode.integrations')
246 config.include('rhodecode.authentication')
247
247 248 # apps
248 249 config.include('rhodecode.apps._base')
249 250 config.include('rhodecode.apps.ops')
250 251 config.include('rhodecode.apps.admin')
251 252 config.include('rhodecode.apps.channelstream')
252 253 config.include('rhodecode.apps.login')
253 254 config.include('rhodecode.apps.home')
254 255 config.include('rhodecode.apps.journal')
255 256 config.include('rhodecode.apps.repository')
256 257 config.include('rhodecode.apps.repo_group')
257 258 config.include('rhodecode.apps.user_group')
258 259 config.include('rhodecode.apps.search')
259 260 config.include('rhodecode.apps.user_profile')
260 261 config.include('rhodecode.apps.user_group_profile')
261 262 config.include('rhodecode.apps.my_account')
262 263 config.include('rhodecode.apps.svn_support')
263 264 config.include('rhodecode.apps.ssh_support')
264 265 config.include('rhodecode.apps.gist')
265 266 config.include('rhodecode.apps.debug_style')
266 267 config.include('rhodecode.api')
267 268
268 269 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
269
270 270 config.add_translation_dirs('rhodecode:i18n/')
271 271 settings['default_locale_name'] = settings.get('lang', 'en')
272 272
273 273 # Add subscribers.
274 274 config.add_subscriber(inject_app_settings,
275 275 pyramid.events.ApplicationCreated)
276 276 config.add_subscriber(scan_repositories_if_enabled,
277 277 pyramid.events.ApplicationCreated)
278 278 config.add_subscriber(write_metadata_if_needed,
279 279 pyramid.events.ApplicationCreated)
280 280 config.add_subscriber(write_js_routes_if_enabled,
281 281 pyramid.events.ApplicationCreated)
282 282
283 283 # request custom methods
284 284 config.add_request_method(
285 285 'rhodecode.lib.partial_renderer.get_partial_renderer',
286 286 'get_partial_renderer')
287 287
288 288 # Set the authorization policy.
289 289 authz_policy = ACLAuthorizationPolicy()
290 290 config.set_authorization_policy(authz_policy)
291 291
292 292 # Set the default renderer for HTML templates to mako.
293 293 config.add_mako_renderer('.html')
294 294
295 295 config.add_renderer(
296 296 name='json_ext',
297 297 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
298 298
299 299 # include RhodeCode plugins
300 300 includes = aslist(settings.get('rhodecode.includes', []))
301 301 for inc in includes:
302 302 config.include(inc)
303 303
304 304 # custom not found view, if our pyramid app doesn't know how to handle
305 305 # the request pass it to potential VCS handling ap
306 306 config.add_notfound_view(not_found_view)
307 307 if not settings.get('debugtoolbar.enabled', False):
308 308 # disabled debugtoolbar handle all exceptions via the error_handlers
309 309 config.add_view(error_handler, context=Exception)
310 310
311 311 # all errors including 403/404/50X
312 312 config.add_view(error_handler, context=HTTPError)
313 313
314 314
315 315 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
316 316 """
317 317 Apply outer WSGI middlewares around the application.
318 318 """
319 319 registry = config.registry
320 320 settings = registry.settings
321 321
322 322 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
323 323 pyramid_app = HttpsFixup(pyramid_app, settings)
324 324
325 325 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
326 326 pyramid_app, settings)
327 327 registry.ae_client = _ae_client
328 328
329 329 if settings['gzip_responses']:
330 330 pyramid_app = make_gzip_middleware(
331 331 pyramid_app, settings, compress_level=1)
332 332
333 333 # this should be the outer most middleware in the wsgi stack since
334 334 # middleware like Routes make database calls
335 335 def pyramid_app_with_cleanup(environ, start_response):
336 336 try:
337 337 return pyramid_app(environ, start_response)
338 338 finally:
339 339 # Dispose current database session and rollback uncommitted
340 340 # transactions.
341 341 meta.Session.remove()
342 342
343 343 # In a single threaded mode server, on non sqlite db we should have
344 344 # '0 Current Checked out connections' at the end of a request,
345 345 # if not, then something, somewhere is leaving a connection open
346 346 pool = meta.Base.metadata.bind.engine.pool
347 347 log.debug('sa pool status: %s', pool.status())
348 348 log.debug('Request processing finalized')
349 349
350 350 return pyramid_app_with_cleanup
351 351
352 352
353 353 def sanitize_settings_and_apply_defaults(settings):
354 354 """
355 355 Applies settings defaults and does all type conversion.
356 356
357 357 We would move all settings parsing and preparation into this place, so that
358 358 we have only one place left which deals with this part. The remaining parts
359 359 of the application would start to rely fully on well prepared settings.
360 360
361 361 This piece would later be split up per topic to avoid a big fat monster
362 362 function.
363 363 """
364 364
365 365 settings.setdefault('rhodecode.edition', 'Community Edition')
366 366
367 367 if 'mako.default_filters' not in settings:
368 368 # set custom default filters if we don't have it defined
369 369 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
370 370 settings['mako.default_filters'] = 'h_filter'
371 371
372 372 if 'mako.directories' not in settings:
373 373 mako_directories = settings.setdefault('mako.directories', [
374 374 # Base templates of the original application
375 375 'rhodecode:templates',
376 376 ])
377 377 log.debug(
378 378 "Using the following Mako template directories: %s",
379 379 mako_directories)
380 380
381 381 # Default includes, possible to change as a user
382 382 pyramid_includes = settings.setdefault('pyramid.includes', [
383 383 'rhodecode.lib.middleware.request_wrapper',
384 384 ])
385 385 log.debug(
386 386 "Using the following pyramid.includes: %s",
387 387 pyramid_includes)
388 388
389 389 # TODO: johbo: Re-think this, usually the call to config.include
390 390 # should allow to pass in a prefix.
391 391 settings.setdefault('rhodecode.api.url', '/_admin/api')
392 392
393 393 # Sanitize generic settings.
394 394 _list_setting(settings, 'default_encoding', 'UTF-8')
395 395 _bool_setting(settings, 'is_test', 'false')
396 396 _bool_setting(settings, 'gzip_responses', 'false')
397 397
398 398 # Call split out functions that sanitize settings for each topic.
399 399 _sanitize_appenlight_settings(settings)
400 400 _sanitize_vcs_settings(settings)
401 401 _sanitize_cache_settings(settings)
402 402
403 403 # configure instance id
404 404 config_utils.set_instance_id(settings)
405 405
406 406 return settings
407 407
408 408
409 409 def _sanitize_appenlight_settings(settings):
410 410 _bool_setting(settings, 'appenlight', 'false')
411 411
412 412
413 413 def _sanitize_vcs_settings(settings):
414 414 """
415 415 Applies settings defaults and does type conversion for all VCS related
416 416 settings.
417 417 """
418 418 _string_setting(settings, 'vcs.svn.compatible_version', '')
419 419 _string_setting(settings, 'git_rev_filter', '--all')
420 420 _string_setting(settings, 'vcs.hooks.protocol', 'http')
421 421 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
422 422 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
423 423 _string_setting(settings, 'vcs.server', '')
424 424 _string_setting(settings, 'vcs.server.log_level', 'debug')
425 425 _string_setting(settings, 'vcs.server.protocol', 'http')
426 426 _bool_setting(settings, 'startup.import_repos', 'false')
427 427 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
428 428 _bool_setting(settings, 'vcs.server.enable', 'true')
429 429 _bool_setting(settings, 'vcs.start_server', 'false')
430 430 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
431 431 _int_setting(settings, 'vcs.connection_timeout', 3600)
432 432
433 433 # Support legacy values of vcs.scm_app_implementation. Legacy
434 434 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
435 435 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
436 436 scm_app_impl = settings['vcs.scm_app_implementation']
437 437 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
438 438 settings['vcs.scm_app_implementation'] = 'http'
439 439
440 440
441 441 def _sanitize_cache_settings(settings):
442 442 temp_store = tempfile.gettempdir()
443 443 default_cache_dir = os.path.join(temp_store, 'rc_cache')
444 444
445 445 # save default, cache dir, and use it for all backends later.
446 446 default_cache_dir = _string_setting(
447 447 settings,
448 448 'cache_dir',
449 449 default_cache_dir, lower=False, default_when_empty=True)
450 450
451 451 # ensure we have our dir created
452 452 if not os.path.isdir(default_cache_dir):
453 453 os.makedirs(default_cache_dir, mode=0755)
454 454
455 455 # exception store cache
456 456 _string_setting(
457 457 settings,
458 458 'exception_tracker.store_path',
459 459 temp_store, lower=False, default_when_empty=True)
460 460
461 461 # cache_perms
462 462 _string_setting(
463 463 settings,
464 464 'rc_cache.cache_perms.backend',
465 465 'dogpile.cache.rc.file_namespace', lower=False)
466 466 _int_setting(
467 467 settings,
468 468 'rc_cache.cache_perms.expiration_time',
469 469 60)
470 470 _string_setting(
471 471 settings,
472 472 'rc_cache.cache_perms.arguments.filename',
473 473 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
474 474
475 475 # cache_repo
476 476 _string_setting(
477 477 settings,
478 478 'rc_cache.cache_repo.backend',
479 479 'dogpile.cache.rc.file_namespace', lower=False)
480 480 _int_setting(
481 481 settings,
482 482 'rc_cache.cache_repo.expiration_time',
483 483 60)
484 484 _string_setting(
485 485 settings,
486 486 'rc_cache.cache_repo.arguments.filename',
487 487 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
488 488
489 489 # cache_license
490 490 _string_setting(
491 491 settings,
492 492 'rc_cache.cache_license.backend',
493 493 'dogpile.cache.rc.file_namespace', lower=False)
494 494 _int_setting(
495 495 settings,
496 496 'rc_cache.cache_license.expiration_time',
497 497 5*60)
498 498 _string_setting(
499 499 settings,
500 500 'rc_cache.cache_license.arguments.filename',
501 501 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
502 502
503 503 # cache_repo_longterm memory, 96H
504 504 _string_setting(
505 505 settings,
506 506 'rc_cache.cache_repo_longterm.backend',
507 507 'dogpile.cache.rc.memory_lru', lower=False)
508 508 _int_setting(
509 509 settings,
510 510 'rc_cache.cache_repo_longterm.expiration_time',
511 511 345600)
512 512 _int_setting(
513 513 settings,
514 514 'rc_cache.cache_repo_longterm.max_size',
515 515 10000)
516 516
517 517 # sql_cache_short
518 518 _string_setting(
519 519 settings,
520 520 'rc_cache.sql_cache_short.backend',
521 521 'dogpile.cache.rc.memory_lru', lower=False)
522 522 _int_setting(
523 523 settings,
524 524 'rc_cache.sql_cache_short.expiration_time',
525 525 30)
526 526 _int_setting(
527 527 settings,
528 528 'rc_cache.sql_cache_short.max_size',
529 529 10000)
530 530
531 531
532 532 def _int_setting(settings, name, default):
533 533 settings[name] = int(settings.get(name, default))
534 534 return settings[name]
535 535
536 536
537 537 def _bool_setting(settings, name, default):
538 538 input_val = settings.get(name, default)
539 539 if isinstance(input_val, unicode):
540 540 input_val = input_val.encode('utf8')
541 541 settings[name] = asbool(input_val)
542 542 return settings[name]
543 543
544 544
545 545 def _list_setting(settings, name, default):
546 546 raw_value = settings.get(name, default)
547 547
548 548 old_separator = ','
549 549 if old_separator in raw_value:
550 550 # If we get a comma separated list, pass it to our own function.
551 551 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
552 552 else:
553 553 # Otherwise we assume it uses pyramids space/newline separation.
554 554 settings[name] = aslist(raw_value)
555 555 return settings[name]
556 556
557 557
558 558 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
559 559 value = settings.get(name, default)
560 560
561 561 if default_when_empty and not value:
562 562 # use default value when value is empty
563 563 value = default
564 564
565 565 if lower:
566 566 value = value.lower()
567 567 settings[name] = value
568 568 return settings[name]
569 569
570 570
571 571 def _substitute_values(mapping, substitutions):
572 572
573 573 try:
574 574 result = {
575 575 # Note: Cannot use regular replacements, since they would clash
576 576 # with the implementation of ConfigParser. Using "format" instead.
577 577 key: value.format(**substitutions)
578 578 for key, value in mapping.items()
579 579 }
580 580 except KeyError as e:
581 581 raise ValueError(
582 582 'Failed to substitute env variable: {}. '
583 583 'Make sure you have specified this env variable without ENV_ prefix'.format(e))
584 584 except ValueError as e:
585 585 log.warning('Failed to substitute ENV variable: %s', e)
586 586 result = mapping
587 587
588 588 return result
@@ -1,37 +1,37 b''
1 1 # -*- coding: utf-8 -*-
2 2 # Copyright (C) 2012-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import logging
21 21 import collections
22 22
23 23 log = logging.getLogger(__name__)
24 24
25 25
26 26 class IntegrationTypeRegistry(collections.OrderedDict):
27 27 """
28 28 Registry Class to hold IntegrationTypes
29 29 """
30 30 def register_integration_type(self, IntegrationType):
31 31 key = IntegrationType.key
32 32 if key in self:
33 33 log.debug(
34 34 'Overriding existing integration type %s (%s) with %s',
35 self[key], key, IntegrationType)
35 self[key].__class__, key, IntegrationType)
36 36
37 37 self[key] = IntegrationType
@@ -1,189 +1,180 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 # Import early to make sure things are patched up properly
22 22 from setuptools import setup, find_packages
23 23
24 24 import os
25 25 import sys
26 26 import pkgutil
27 27 import platform
28 28 import codecs
29 29
30 30 try: # for pip >= 10
31 31 from pip._internal.req import parse_requirements
32 32 except ImportError: # for pip <= 9.0.3
33 33 from pip.req import parse_requirements
34 34
35 35 try: # for pip >= 10
36 36 from pip._internal.download import PipSession
37 37 except ImportError: # for pip <= 9.0.3
38 38 from pip.download import PipSession
39 39
40 40
41 41 if sys.version_info < (2, 7):
42 42 raise Exception('RhodeCode requires Python 2.7 or later')
43 43
44 44 here = os.path.abspath(os.path.dirname(__file__))
45 45
46 46 # defines current platform
47 47 __platform__ = platform.system()
48 48 __license__ = 'AGPLv3, and Commercial License'
49 49 __author__ = 'RhodeCode GmbH'
50 50 __url__ = 'https://code.rhodecode.com'
51 51 is_windows = __platform__ in ('Windows',)
52 52
53 53
54 54 def _get_requirements(req_filename, exclude=None, extras=None):
55 55 extras = extras or []
56 56 exclude = exclude or []
57 57
58 58 try:
59 59 parsed = parse_requirements(
60 60 os.path.join(here, req_filename), session=PipSession())
61 61 except TypeError:
62 62 # try pip < 6.0.0, that doesn't support session
63 63 parsed = parse_requirements(os.path.join(here, req_filename))
64 64
65 65 requirements = []
66 66 for ir in parsed:
67 67 if ir.req and ir.name not in exclude:
68 68 requirements.append(str(ir.req))
69 69 return requirements + extras
70 70
71 71
72 72 # requirements extract
73 73 setup_requirements = ['PasteScript', 'pytest-runner']
74 74 install_requirements = _get_requirements(
75 75 'requirements.txt', exclude=['setuptools', 'entrypoints'])
76 76 test_requirements = _get_requirements(
77 77 'requirements_test.txt', extras=['configobj'])
78 78
79 79
80 80 def get_version():
81 81 version = pkgutil.get_data('rhodecode', 'VERSION')
82 82 return version.strip()
83 83
84 84
85 85 # additional files that goes into package itself
86 86 package_data = {
87 87 '': ['*.txt', '*.rst'],
88 88 'configs': ['*.ini'],
89 89 'rhodecode': ['VERSION', 'i18n/*/LC_MESSAGES/*.mo', ],
90 90 }
91 91
92 92 description = 'Source Code Management Platform'
93 93 keywords = ' '.join([
94 94 'rhodecode', 'mercurial', 'git', 'svn',
95 95 'code review',
96 96 'repo groups', 'ldap', 'repository management', 'hgweb',
97 97 'hgwebdir', 'gitweb', 'serving hgweb',
98 98 ])
99 99
100 100
101 101 # README/DESCRIPTION generation
102 102 readme_file = 'README.rst'
103 103 changelog_file = 'CHANGES.rst'
104 104 try:
105 105 long_description = codecs.open(readme_file).read() + '\n\n' + \
106 106 codecs.open(changelog_file).read()
107 107 except IOError as err:
108 108 sys.stderr.write(
109 109 "[WARNING] Cannot find file specified as long_description (%s)\n "
110 110 "or changelog (%s) skipping that file" % (readme_file, changelog_file))
111 111 long_description = description
112 112
113 113
114 114 setup(
115 115 name='rhodecode-enterprise-ce',
116 116 version=get_version(),
117 117 description=description,
118 118 long_description=long_description,
119 119 keywords=keywords,
120 120 license=__license__,
121 121 author=__author__,
122 122 author_email='support@rhodecode.com',
123 123 url=__url__,
124 124 setup_requires=setup_requirements,
125 125 install_requires=install_requirements,
126 126 tests_require=test_requirements,
127 127 zip_safe=False,
128 128 packages=find_packages(exclude=["docs", "tests*"]),
129 129 package_data=package_data,
130 130 include_package_data=True,
131 131 classifiers=[
132 132 'Development Status :: 6 - Mature',
133 133 'Environment :: Web Environment',
134 134 'Intended Audience :: Developers',
135 135 'Operating System :: OS Independent',
136 136 'Topic :: Software Development :: Version Control',
137 137 'License :: OSI Approved :: Affero GNU General Public License v3 or later (AGPLv3+)',
138 138 'Programming Language :: Python :: 2.7',
139 139 ],
140 140 message_extractors={
141 141 'rhodecode': [
142 142 ('**.py', 'python', None),
143 143 ('**.js', 'javascript', None),
144 144 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
145 145 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
146 146 ('public/**', 'ignore', None),
147 147 ]
148 148 },
149 149 paster_plugins=['PasteScript'],
150 150 entry_points={
151 'enterprise.plugins1': [
152 'crowd=rhodecode.authentication.plugins.auth_crowd:plugin_factory',
153 'headers=rhodecode.authentication.plugins.auth_headers:plugin_factory',
154 'jasig_cas=rhodecode.authentication.plugins.auth_jasig_cas:plugin_factory',
155 'ldap=rhodecode.authentication.plugins.auth_ldap:plugin_factory',
156 'pam=rhodecode.authentication.plugins.auth_pam:plugin_factory',
157 'rhodecode=rhodecode.authentication.plugins.auth_rhodecode:plugin_factory',
158 'token=rhodecode.authentication.plugins.auth_token:plugin_factory',
159 ],
160 151 'paste.app_factory': [
161 152 'main=rhodecode.config.middleware:make_pyramid_app',
162 153 ],
163 154 'paste.global_paster_command': [
164 155 'ishell=rhodecode.lib.paster_commands.ishell:Command',
165 156 'upgrade-db=rhodecode.lib.paster_commands.upgrade_db:UpgradeDb',
166 157
167 158 'setup-rhodecode=rhodecode.lib.paster_commands.deprecated.setup_rhodecode:Command',
168 159 'celeryd=rhodecode.lib.paster_commands.deprecated.celeryd:Command',
169 160 ],
170 161 'pyramid.pshell_runner': [
171 162 'ipython = rhodecode.lib.pyramid_shell:ipython_shell_runner',
172 163 ],
173 164 'pytest11': [
174 165 'pylons=rhodecode.tests.pylons_plugin',
175 166 'enterprise=rhodecode.tests.plugin',
176 167 ],
177 168 'console_scripts': [
178 169 'rc-server=rhodecode.rcserver:main',
179 170 'rc-setup-app=rhodecode.lib.rc_commands.setup_rc:main',
180 171 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
181 172 'rc-ishell=rhodecode.lib.rc_commands.ishell:main',
182 173 'rc-ssh-wrapper=rhodecode.apps.ssh_support.lib.ssh_wrapper:main',
183 174 ],
184 175 'beaker.backends': [
185 176 'memorylru_base=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase',
186 177 'memorylru_debug=rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerDebug'
187 178 ]
188 179 },
189 180 )
General Comments 0
You need to be logged in to leave comments. Login now