Show More
@@ -0,0 +1,48 b'' | |||||
|
1 | |RCE| 4.12.1 |RNS| | |||
|
2 | ------------------ | |||
|
3 | ||||
|
4 | Release Date | |||
|
5 | ^^^^^^^^^^^^ | |||
|
6 | ||||
|
7 | - 2018-05-15 | |||
|
8 | ||||
|
9 | ||||
|
10 | New Features | |||
|
11 | ^^^^^^^^^^^^ | |||
|
12 | ||||
|
13 | ||||
|
14 | ||||
|
15 | General | |||
|
16 | ^^^^^^^ | |||
|
17 | ||||
|
18 | - SVN: execute web-based hooks for SVN so integration framework work also via | |||
|
19 | web based editor. | |||
|
20 | - ReCaptcha: adjust for v2 that is the only left one supported since 1st of May. | |||
|
21 | - JIRA integration: add support for proxy server connection to JIRA server. | |||
|
22 | ||||
|
23 | ||||
|
24 | Security | |||
|
25 | ^^^^^^^^ | |||
|
26 | ||||
|
27 | ||||
|
28 | ||||
|
29 | Performance | |||
|
30 | ^^^^^^^^^^^ | |||
|
31 | ||||
|
32 | ||||
|
33 | ||||
|
34 | Fixes | |||
|
35 | ^^^^^ | |||
|
36 | ||||
|
37 | - SVN: make hooks safer and fully backward compatible. In certain old setups | |||
|
38 | new integration for SVN could make problems. We use a safer hooks now that | |||
|
39 | shouldn't break usage of older SVN and still provide required functionality | |||
|
40 | for integration framework | |||
|
41 | - LDAP: use connection ping only in case of single server. | |||
|
42 | - Repository feed: fix path-based permissions condition on caching element. | |||
|
43 | ||||
|
44 | ||||
|
45 | Upgrade notes | |||
|
46 | ^^^^^^^^^^^^^ | |||
|
47 | ||||
|
48 | - Scheduled release addressing found problems reported by users. |
@@ -36,3 +36,4 b' 194c74f33e32bbae6fc4d71ec5a999cff3c13605' | |||||
36 | 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5 |
|
36 | 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5 | |
37 | f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6 |
|
37 | f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6 | |
38 | b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0 |
|
38 | b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0 | |
|
39 | 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1 |
@@ -9,6 +9,7 b' Release Notes' | |||||
9 | .. toctree:: |
|
9 | .. toctree:: | |
10 | :maxdepth: 1 |
|
10 | :maxdepth: 1 | |
11 |
|
11 | |||
|
12 | release-notes-4.12.1.rst | |||
12 | release-notes-4.12.0.rst |
|
13 | release-notes-4.12.0.rst | |
13 | release-notes-4.11.6.rst |
|
14 | release-notes-4.11.6.rst | |
14 | release-notes-4.11.5.rst |
|
15 | release-notes-4.11.5.rst |
@@ -152,13 +152,14 b' class RepoFeedView(RepoAppView):' | |||||
152 | return _generate_feed() |
|
152 | return _generate_feed() | |
153 |
|
153 | |||
154 | if self.path_filter.is_enabled: |
|
154 | if self.path_filter.is_enabled: | |
|
155 | mime_type, feed = _generate_feed() | |||
|
156 | else: | |||
155 | invalidator_context = CacheKey.repo_context_cache( |
|
157 | invalidator_context = CacheKey.repo_context_cache( | |
156 |
_generate_feed_and_cache, self.db_repo_name, |
|
158 | _generate_feed_and_cache, self.db_repo_name, | |
|
159 | CacheKey.CACHE_TYPE_ATOM) | |||
157 | with invalidator_context as context: |
|
160 | with invalidator_context as context: | |
158 | context.invalidate() |
|
161 | context.invalidate() | |
159 | mime_type, feed = context.compute() |
|
162 | mime_type, feed = context.compute() | |
160 | else: |
|
|||
161 | mime_type, feed = _generate_feed() |
|
|||
162 |
|
163 | |||
163 | response = Response(feed) |
|
164 | response = Response(feed) | |
164 | response.content_type = mime_type |
|
165 | response.content_type = mime_type | |
@@ -204,14 +205,15 b' class RepoFeedView(RepoAppView):' | |||||
204 | return _generate_feed() |
|
205 | return _generate_feed() | |
205 |
|
206 | |||
206 | if self.path_filter.is_enabled: |
|
207 | if self.path_filter.is_enabled: | |
|
208 | mime_type, feed = _generate_feed() | |||
|
209 | else: | |||
207 | invalidator_context = CacheKey.repo_context_cache( |
|
210 | invalidator_context = CacheKey.repo_context_cache( | |
208 |
_generate_feed_and_cache, self.db_repo_name, |
|
211 | _generate_feed_and_cache, self.db_repo_name, | |
|
212 | CacheKey.CACHE_TYPE_RSS) | |||
209 |
|
213 | |||
210 | with invalidator_context as context: |
|
214 | with invalidator_context as context: | |
211 | context.invalidate() |
|
215 | context.invalidate() | |
212 | mime_type, feed = context.compute() |
|
216 | mime_type, feed = context.compute() | |
213 | else: |
|
|||
214 | mime_type, feed = _generate_feed() |
|
|||
215 |
|
217 | |||
216 | response = Response(feed) |
|
218 | response = Response(feed) | |
217 | response.content_type = mime_type |
|
219 | response.content_type = mime_type |
@@ -21,7 +21,8 b'' | |||||
21 | """ |
|
21 | """ | |
22 | Authentication modules |
|
22 | Authentication modules | |
23 | """ |
|
23 | """ | |
24 |
|
24 | import socket | ||
|
25 | import string | |||
25 | import colander |
|
26 | import colander | |
26 | import copy |
|
27 | import copy | |
27 | import logging |
|
28 | import logging | |
@@ -31,14 +32,13 b' import warnings' | |||||
31 | import functools |
|
32 | import functools | |
32 |
|
33 | |||
33 | from pyramid.threadlocal import get_current_registry |
|
34 | from pyramid.threadlocal import get_current_registry | |
34 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
|||
35 |
|
35 | |||
36 | from rhodecode.authentication.interface import IAuthnPluginRegistry |
|
36 | from rhodecode.authentication.interface import IAuthnPluginRegistry | |
37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
38 | from rhodecode.lib import caches |
|
38 | from rhodecode.lib import caches | |
39 | from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt |
|
39 | from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt | |
40 | from rhodecode.lib.utils2 import safe_int |
|
40 | from rhodecode.lib.utils2 import safe_int, safe_str | |
41 |
from rhodecode.lib. |
|
41 | from rhodecode.lib.exceptions import LdapConnectionError | |
42 | from rhodecode.model.db import User |
|
42 | from rhodecode.model.db import User | |
43 | from rhodecode.model.meta import Session |
|
43 | from rhodecode.model.meta import Session | |
44 | from rhodecode.model.settings import SettingsModel |
|
44 | from rhodecode.model.settings import SettingsModel | |
@@ -556,6 +556,63 b' class RhodeCodeExternalAuthPlugin(RhodeC' | |||||
556 | return auth |
|
556 | return auth | |
557 |
|
557 | |||
558 |
|
558 | |||
|
559 | class AuthLdapBase(object): | |||
|
560 | ||||
|
561 | @classmethod | |||
|
562 | def _build_servers(cls, ldap_server_type, ldap_server, port): | |||
|
563 | def host_resolver(host, port, full_resolve=True): | |||
|
564 | """ | |||
|
565 | Main work for this function is to prevent ldap connection issues, | |||
|
566 | and detect them early using a "greenified" sockets | |||
|
567 | """ | |||
|
568 | host = host.strip() | |||
|
569 | if not full_resolve: | |||
|
570 | return '{}:{}'.format(host, port) | |||
|
571 | ||||
|
572 | log.debug('LDAP: Resolving IP for LDAP host %s', host) | |||
|
573 | try: | |||
|
574 | ip = socket.gethostbyname(host) | |||
|
575 | log.debug('Got LDAP server %s ip %s', host, ip) | |||
|
576 | except Exception: | |||
|
577 | raise LdapConnectionError( | |||
|
578 | 'Failed to resolve host: `{}`'.format(host)) | |||
|
579 | ||||
|
580 | log.debug('LDAP: Checking if IP %s is accessible', ip) | |||
|
581 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
|
582 | try: | |||
|
583 | s.connect((ip, int(port))) | |||
|
584 | s.shutdown(socket.SHUT_RD) | |||
|
585 | except Exception: | |||
|
586 | raise LdapConnectionError( | |||
|
587 | 'Failed to connect to host: `{}:{}`'.format(host, port)) | |||
|
588 | ||||
|
589 | return '{}:{}'.format(host, port) | |||
|
590 | ||||
|
591 | if len(ldap_server) == 1: | |||
|
592 | # in case of single server use resolver to detect potential | |||
|
593 | # connection issues | |||
|
594 | full_resolve = True | |||
|
595 | else: | |||
|
596 | full_resolve = False | |||
|
597 | ||||
|
598 | return ', '.join( | |||
|
599 | ["{}://{}".format( | |||
|
600 | ldap_server_type, | |||
|
601 | host_resolver(host, port, full_resolve=full_resolve)) | |||
|
602 | for host in ldap_server]) | |||
|
603 | ||||
|
604 | @classmethod | |||
|
605 | def _get_server_list(cls, servers): | |||
|
606 | return map(string.strip, servers.split(',')) | |||
|
607 | ||||
|
608 | @classmethod | |||
|
609 | def get_uid(cls, username, server_addresses): | |||
|
610 | uid = username | |||
|
611 | for server_addr in server_addresses: | |||
|
612 | uid = chop_at(username, "@%s" % server_addr) | |||
|
613 | return uid | |||
|
614 | ||||
|
615 | ||||
559 | def loadplugin(plugin_id): |
|
616 | def loadplugin(plugin_id): | |
560 | """ |
|
617 | """ | |
561 | Loads and returns an instantiated authentication plugin. |
|
618 | Loads and returns an instantiated authentication plugin. | |
@@ -613,7 +670,7 b' def authenticate(username, password, env' | |||||
613 | authn_registry = get_authn_registry(registry) |
|
670 | authn_registry = get_authn_registry(registry) | |
614 | plugins_to_check = authn_registry.get_plugins_for_authentication() |
|
671 | plugins_to_check = authn_registry.get_plugins_for_authentication() | |
615 | log.debug('Starting ordered authentication chain using %s plugins', |
|
672 | log.debug('Starting ordered authentication chain using %s plugins', | |
616 | plugins_to_check) |
|
673 | [x.name for x in plugins_to_check]) | |
617 | for plugin in plugins_to_check: |
|
674 | for plugin in plugins_to_check: | |
618 | plugin.set_auth_type(auth_type) |
|
675 | plugin.set_auth_type(auth_type) | |
619 | plugin.set_calling_scope_repo(acl_repo_name) |
|
676 | plugin.set_calling_scope_repo(acl_repo_name) |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2012-2018 RhodeCode GmbH | |||
|
4 | # | |||
|
5 | # This program is free software: you can redistribute it and/or modify | |||
|
6 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
7 | # (only), as published by the Free Software Foundation. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU Affero General Public License | |||
|
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
16 | # | |||
|
17 | # This program is dual-licensed. If you wish to learn more about the | |||
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
@@ -22,16 +22,13 b'' | |||||
22 | RhodeCode authentication plugin for LDAP |
|
22 | RhodeCode authentication plugin for LDAP | |
23 | """ |
|
23 | """ | |
24 |
|
24 | |||
25 | import socket |
|
|||
26 | import re |
|
|||
27 | import colander |
|
|||
28 | import logging |
|
25 | import logging | |
29 | import traceback |
|
26 | import traceback | |
30 | import string |
|
|||
31 |
|
27 | |||
|
28 | import colander | |||
32 | from rhodecode.translation import _ |
|
29 | from rhodecode.translation import _ | |
33 | from rhodecode.authentication.base import ( |
|
30 | from rhodecode.authentication.base import ( | |
34 |
RhodeCodeExternalAuthPlugin, |
|
31 | RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property) | |
35 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
32 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase | |
36 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
33 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
37 | from rhodecode.lib.colander_utils import strip_whitespace |
|
34 | from rhodecode.lib.colander_utils import strip_whitespace | |
@@ -200,40 +197,7 b' class LdapSettingsSchema(AuthnPluginSett' | |||||
200 | widget='string') |
|
197 | widget='string') | |
201 |
|
198 | |||
202 |
|
199 | |||
203 |
class AuthLdap( |
|
200 | class AuthLdap(AuthLdapBase): | |
204 |
|
||||
205 | def _build_servers(self): |
|
|||
206 | def host_resolver(host, port): |
|
|||
207 | """ |
|
|||
208 | Main work for this function is to prevent ldap connection issues, |
|
|||
209 | and detect them early using a "greenified" sockets |
|
|||
210 | """ |
|
|||
211 | host = host.strip() |
|
|||
212 |
|
||||
213 | log.info('Resolving LDAP host %s', host) |
|
|||
214 | try: |
|
|||
215 | ip = socket.gethostbyname(host) |
|
|||
216 | log.info('Got LDAP server %s ip %s', host, ip) |
|
|||
217 | except Exception: |
|
|||
218 | raise LdapConnectionError( |
|
|||
219 | 'Failed to resolve host: `{}`'.format(host)) |
|
|||
220 |
|
||||
221 | log.info('Checking LDAP IP access %s', ip) |
|
|||
222 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|||
223 | try: |
|
|||
224 | s.connect((ip, int(port))) |
|
|||
225 | s.shutdown(socket.SHUT_RD) |
|
|||
226 | except Exception: |
|
|||
227 | raise LdapConnectionError( |
|
|||
228 | 'Failed to connect to host: `{}:{}`'.format(host, port)) |
|
|||
229 |
|
||||
230 | return '{}:{}'.format(host, port) |
|
|||
231 |
|
||||
232 | port = self.LDAP_SERVER_PORT |
|
|||
233 | return ', '.join( |
|
|||
234 | ["{}://{}".format( |
|
|||
235 | self.ldap_server_type, host_resolver(host, port)) |
|
|||
236 | for host in self.SERVER_ADDRESSES]) |
|
|||
237 |
|
201 | |||
238 | def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='', |
|
202 | def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='', | |
239 | tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, |
|
203 | tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, | |
@@ -256,8 +220,9 b' class AuthLdap(object):' | |||||
256 | OPT_X_TLS_DEMAND = 2 |
|
220 | OPT_X_TLS_DEMAND = 2 | |
257 | self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, |
|
221 | self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, | |
258 | OPT_X_TLS_DEMAND) |
|
222 | OPT_X_TLS_DEMAND) | |
|
223 | self.LDAP_SERVER = server | |||
259 | # split server into list |
|
224 | # split server into list | |
260 |
self.SERVER_ADDRESSES = server |
|
225 | self.SERVER_ADDRESSES = self._get_server_list(server) | |
261 | self.LDAP_SERVER_PORT = port |
|
226 | self.LDAP_SERVER_PORT = port | |
262 |
|
227 | |||
263 | # USE FOR READ ONLY BIND TO LDAP SERVER |
|
228 | # USE FOR READ ONLY BIND TO LDAP SERVER | |
@@ -265,13 +230,12 b' class AuthLdap(object):' | |||||
265 |
|
230 | |||
266 | self.LDAP_BIND_DN = safe_str(bind_dn) |
|
231 | self.LDAP_BIND_DN = safe_str(bind_dn) | |
267 | self.LDAP_BIND_PASS = safe_str(bind_pass) |
|
232 | self.LDAP_BIND_PASS = safe_str(bind_pass) | |
268 | self.LDAP_SERVER = self._build_servers() |
|
233 | ||
269 | self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope) |
|
234 | self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope) | |
270 | self.BASE_DN = safe_str(base_dn) |
|
235 | self.BASE_DN = safe_str(base_dn) | |
271 | self.LDAP_FILTER = safe_str(ldap_filter) |
|
236 | self.LDAP_FILTER = safe_str(ldap_filter) | |
272 |
|
237 | |||
273 | def _get_ldap_conn(self): |
|
238 | def _get_ldap_conn(self): | |
274 | log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER) |
|
|||
275 |
|
239 | |||
276 | if self.debug: |
|
240 | if self.debug: | |
277 | ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255) |
|
241 | ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255) | |
@@ -285,7 +249,10 b' class AuthLdap(object):' | |||||
285 | ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON) |
|
249 | ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON) | |
286 |
|
250 | |||
287 | # init connection now |
|
251 | # init connection now | |
288 | ldap_conn = ldap.initialize(self.LDAP_SERVER) |
|
252 | ldap_servers = self._build_servers( | |
|
253 | self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT) | |||
|
254 | log.debug('initializing LDAP connection to:%s', ldap_servers) | |||
|
255 | ldap_conn = ldap.initialize(ldap_servers) | |||
289 | ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout) |
|
256 | ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout) | |
290 | ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout) |
|
257 | ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout) | |
291 | ldap_conn.timeout = self.timeout |
|
258 | ldap_conn.timeout = self.timeout | |
@@ -305,12 +272,6 b' class AuthLdap(object):' | |||||
305 |
|
272 | |||
306 | return ldap_conn |
|
273 | return ldap_conn | |
307 |
|
274 | |||
308 | def get_uid(self, username): |
|
|||
309 | uid = username |
|
|||
310 | for server_addr in self.SERVER_ADDRESSES: |
|
|||
311 | uid = chop_at(username, "@%s" % server_addr) |
|
|||
312 | return uid |
|
|||
313 |
|
||||
314 | def fetch_attrs_from_simple_bind(self, server, dn, username, password): |
|
275 | def fetch_attrs_from_simple_bind(self, server, dn, username, password): | |
315 | try: |
|
276 | try: | |
316 | log.debug('Trying simple bind with %s', dn) |
|
277 | log.debug('Trying simple bind with %s', dn) | |
@@ -336,7 +297,9 b' class AuthLdap(object):' | |||||
336 | :param password: password |
|
297 | :param password: password | |
337 | """ |
|
298 | """ | |
338 |
|
299 | |||
339 | uid = self.get_uid(username) |
|
300 | uid = self.get_uid(username, self.SERVER_ADDRESSES) | |
|
301 | user_attrs = {} | |||
|
302 | dn = '' | |||
340 |
|
303 | |||
341 | if not password: |
|
304 | if not password: | |
342 | msg = "Authenticating user %s with blank password not allowed" |
|
305 | msg = "Authenticating user %s with blank password not allowed" | |
@@ -350,8 +313,8 b' class AuthLdap(object):' | |||||
350 | ldap_conn = self._get_ldap_conn() |
|
313 | ldap_conn = self._get_ldap_conn() | |
351 | filter_ = '(&%s(%s=%s))' % ( |
|
314 | filter_ = '(&%s(%s=%s))' % ( | |
352 | self.LDAP_FILTER, self.attr_login, username) |
|
315 | self.LDAP_FILTER, self.attr_login, username) | |
353 | log.debug("Authenticating %r filter %s at %s", self.BASE_DN, |
|
316 | log.debug( | |
354 | filter_, self.LDAP_SERVER) |
|
317 | "Authenticating %r filter %s", self.BASE_DN, filter_) | |
355 | lobjects = ldap_conn.search_ext_s( |
|
318 | lobjects = ldap_conn.search_ext_s( | |
356 | self.BASE_DN, self.SEARCH_SCOPE, filter_) |
|
319 | self.BASE_DN, self.SEARCH_SCOPE, filter_) | |
357 |
|
320 |
@@ -57,9 +57,6 b' def trigger_post_push_hook(' | |||||
57 | :param repo_alias: the type of SCM repo |
|
57 | :param repo_alias: the type of SCM repo | |
58 | :param commit_ids: list of commit ids that we pushed |
|
58 | :param commit_ids: list of commit ids that we pushed | |
59 | """ |
|
59 | """ | |
60 | if repo_alias not in ('hg', 'git'): |
|
|||
61 | return |
|
|||
62 |
|
||||
63 | extras = _get_rc_scm_extras(username, repo_name, repo_alias, action) |
|
60 | extras = _get_rc_scm_extras(username, repo_name, repo_alias, action) | |
64 | extras.commit_ids = commit_ids |
|
61 | extras.commit_ids = commit_ids | |
65 | hooks_base.post_push(extras) |
|
62 | hooks_base.post_push(extras) |
General Comments 0
You need to be logged in to leave comments.
Login now