Module: check_mk
Branch: master
Commit: a9bd04fa333a16e3f48cac42b22b0d7cd1d0da30
URL: http://git.mathias-kettner.de/git/?p=check_mk.git;a=commit;h=a9bd04fa333a16…
Author: Lars Michelsen <lm(a)mathias-kettner.de>
Date: Tue Oct 22 08:10:43 2019 +0200
Fixed processing of HTTP vars with equal sign in name
This issue was introduced during some internal cleanups where the
valid characters for variable names have been made stricter. This
disallowed base64 encoded parts in the names which is now possible
again.
This is used at least on the user edit dialog and the service discovery
page.
Change-Id: Ia896b7f9a88f91bce806ff178d9a13744339b6a9
---
cmk/gui/http.py | 2 +-
tests/unit/cmk/gui/test_http.py | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/cmk/gui/http.py b/cmk/gui/http.py
index 9d98f90..cd30716 100644
--- a/cmk/gui/http.py
+++ b/cmk/gui/http.py
@@ -83,7 +83,7 @@ class Request(object):
# alphanumeric characters plus any character from set('%*+-._'), which is probably still a
# bit too broad. We should really figure out what we need and make sure that we only use
# that restricted set.
- varname_regex = re.compile(r'^[\w.%*+-]+$')
+ varname_regex = re.compile(r'^[\w.%*+=-]+$')
for field in fields.list:
varname = field.name
diff --git a/tests/unit/cmk/gui/test_http.py b/tests/unit/cmk/gui/test_http.py
index 196175d..bcf03d0 100644
--- a/tests/unit/cmk/gui/test_http.py
+++ b/tests/unit/cmk/gui/test_http.py
@@ -2,11 +2,27 @@
# encoding: utf-8
import time
+import io
import cmk.gui.http as http
from cmk.gui.globals import html
+def test_http_request_allowed_vars():
+ wsgi_environ = {
+ # Please note: This is no complete WSGI environment
+ "REQUEST_METHOD" : "POST",
+ # Contains a variable that has a base64 coded value in it's name. This
+ # is done for example on the service discovery page or on the user
+ # editing page.
+ "wsgi.input" : io.BytesIO("asd=x&_Y21rYWRtaW4%3D=aaa"),
+ "SCRIPT_NAME" : "",
+ "REQUEST_URI" : "",
+ }
+ req = http.Request(wsgi_environ)
+ assert req.var("asd") == "x"
+ assert req.var("_Y21rYWRtaW4=") == "aaa"
+
def test_cookie_handling(register_builtin_html, monkeypatch):
monkeypatch.setattr(html.request, "cookies", {"cookie1": {"key": "1a"}})
assert html.request.get_cookie_names() == ["cookie1"]
Module: check_mk
Branch: master
Commit: 7c4db93c6d3a844e5b5dd068c98dfa72a39f02f3
URL: http://git.mathias-kettner.de/git/?p=check_mk.git;a=commit;h=7c4db93c6d3a84…
Author: Lars Michelsen <lm(a)mathias-kettner.de>
Date: Fri Jul 6 11:45:46 2018 +0200
Improved nested group lookup performance
* Don't rely on the incedibly slow memberOf:1.2.840.113556.1.4.1941:
chain matching mechanism anymore.
* Use nested resolutions of group memberships including runtime cache
of group memberships which is renewed for each sync process.
* Group members are now sorted for better testability
Change-Id: Ieb992a9a03b251b2a14d2fd3b26f353706e0e59a
---
cmk/gui/plugins/userdb/ldap_connector.py | 98 +++++++++++++++++++++++++++-----
1 file changed, 85 insertions(+), 13 deletions(-)
diff --git a/cmk/gui/plugins/userdb/ldap_connector.py b/cmk/gui/plugins/userdb/ldap_connector.py
index e6936a4..4f6970b 100644
--- a/cmk/gui/plugins/userdb/ldap_connector.py
+++ b/cmk/gui/plugins/userdb/ldap_connector.py
@@ -181,6 +181,7 @@ class LDAPUserConnector(UserConnector):
self._num_queries = 0
self._user_cache = {}
self._group_cache = {}
+ self._group_search_cache = {}
# File for storing the time of the last success event
self._sync_time_file = cmk.paths.var_dir + '/web/ldap_%s_sync_time.mk'% self.id()
@@ -765,15 +766,17 @@ class LDAPUserConnector(UserConnector):
def get_group_memberships(self, filters, filt_attr = 'cn', nested = False):
cache_key = (tuple(filters), nested, filt_attr)
- if cache_key in self._group_cache:
- return self._group_cache[cache_key]
+ if cache_key in self._group_search_cache:
+ return self._group_search_cache[cache_key]
+
+ self._group_cache.setdefault(nested, {})
if not nested:
groups = self._get_direct_group_memberships(filters, filt_attr)
else:
groups = self._get_nested_group_memberships(filters, filt_attr)
- self._group_cache[cache_key] = groups
+ self._group_search_cache[cache_key] = groups
return groups
@@ -797,18 +800,27 @@ class LDAPUserConnector(UserConnector):
self._config['group_scope']):
groups[dn] = {
'cn' : obj['cn'][0],
- 'members' : [ m.lower() for m in obj.get(member_attr,[]) ],
+ 'members' : sorted([ m.lower() for m in obj.get(member_attr,[]) ]),
}
else:
# Special handling for OpenLDAP when searching for groups by DN
for f_dn in filters:
+ # Try to get members from group cache
+ try:
+ groups[f_dn] = self._group_cache[False][f_dn]
+ continue
+ except KeyError:
+ pass
+
for dn, obj in self._ldap_search(self._replace_macros(f_dn), filt,
['cn', member_attr], 'base'):
groups[f_dn] = {
'cn' : obj['cn'][0],
- 'members' : [ m.lower() for m in obj.get(member_attr,[]) ],
+ 'members' : sorted([ m.lower() for m in obj.get(member_attr,[]) ]),
}
+ self._group_cache[False].update(groups)
+
return groups
@@ -817,9 +829,17 @@ class LDAPUserConnector(UserConnector):
# memberof filter to get all group memberships of that group. We need one query for each group.
def _get_nested_group_memberships(self, filters, filt_attr):
groups = {}
+
+ # Search group members in common ancestor of group and user base DN to be able to use a single
+ # query instead of one for groups and one for users below when searching for the members.
+ base_dn = self._group_and_user_base_dn()
+
for filter_val in filters:
matched_groups = {}
+ # The memberof query below is only possible when knowing the DN of groups. We need
+ # to look for the DN when the caller gives us CNs (e.g. when using the the groups
+ # to contact groups plugin).
if filt_attr == 'cn':
result = self._ldap_search(self.get_group_dn(),
'(&%s(cn=%s))' % (self.ldap_filter('groups'), filter_val),
@@ -832,22 +852,73 @@ class LDAPUserConnector(UserConnector):
else:
# in case of asking with DNs in nested mode, the resulting objects have the
# cn set to None for all objects. We do not need it in that case.
- matched_groups[filter_val] = None
+ dn = filter_val
+ matched_groups[dn] = None
+ # Now lookup the memberships. Previously we used the filter "memberOf:1.2.840.113556.1.4.1941:"
+ # here which seemed to be a performance problem. Resolving the nesting involves more single
+ # queries but performs much better.
for dn, cn in matched_groups.items():
- filt = '(&%s(memberOf:1.2.840.113556.1.4.1941:=%s))' % \
- (self.ldap_filter('users'), dn)
+ # Try to get members from group cache
+ try:
+ groups[dn] = self._group_cache[True][dn]
+ continue
+ except KeyError:
+ pass
+
+ # In case we don't have the cn we need to fetch it. It may be needed, e.g. by the contact group
+ # sync plugin
+ if cn is None:
+ group = self._ldap_search(dn, filt="(objectclass=group)", columns=['cn'], scope='base')
+ if group:
+ cn = group[0][1]["cn"][0]
+
+ filt = '(memberof=%s)' % dn
groups[dn] = {
- 'members' : [],
- 'cn' : cn,
+ 'members' : [],
+ 'cn' : cn,
}
- for user_dn, _obj in self._ldap_search(self._get_user_dn(),
- filt, ['dn'], self._config['user_scope']):
- groups[dn]['members'].append(user_dn.lower())
+
+ sub_group_filters = []
+ for obj_dn, obj in self._ldap_search(base_dn, filt, ['dn', 'objectclass'], 'sub'):
+ if "user" in obj['objectclass']:
+ groups[dn]['members'].append(obj_dn)
+
+ elif "group" in obj['objectclass']:
+ sub_group_filters.append(obj_dn)
+
+ # TODO: This could be optimized by first collecting all sub groups of all searched
+ # groups, then collecting them all together
+ for _sub_group_dn, sub_group in self.get_group_memberships(sub_group_filters, filt_attr='dn', nested=True).items():
+ groups[dn]['members'] += sub_group["members"]
+
+ groups[dn]['members'].sort()
+
+ self._group_cache[True][dn] = groups[dn]
return groups
+ def _group_and_user_base_dn(self):
+ user_dn = ldap.dn.str2dn(self._get_user_dn())
+ group_dn = ldap.dn.str2dn(self.get_group_dn())
+
+ common_len = min(len(user_dn), len(group_dn))
+ user_dn, group_dn = user_dn[-common_len:], group_dn[-common_len:]
+
+ base_dn = None
+ for i in range(common_len):
+ if user_dn[i:] == group_dn[i:]:
+ base_dn = user_dn[i:]
+ break
+
+ if base_dn is None:
+ raise MKLDAPException(_("Unable to synchronize nested groups (Found no common base DN for user base "
+ "DN \"%s\" and group base DN \"%s\")") % (self._get_user_dn(), self.get_group_dn()))
+
+ return ldap.dn.dn2str(base_dn)
+
+
#
# USERDB API METHODS
#
@@ -1137,6 +1208,7 @@ class LDAPUserConnector(UserConnector):
self._num_queries = 0
self._user_cache.clear()
self._group_cache.clear()
+ self._group_search_cache.clear()
def _set_last_sync_time(self):