From 5aac7488ec6104ca63935d24c4f40e969ca5cad7 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Fri, 1 Nov 2019 16:36:58 +0100 Subject: [PATCH 1/5] search_engine: Make requests assemble the GET params The manual approach was vulnerable to injection of & and thus additional parameters --- glpi/glpi.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/glpi/glpi.py b/glpi/glpi.py index 9e67b45..9ba50c8 100644 --- a/glpi/glpi.py +++ b/glpi/glpi.py @@ -397,7 +397,7 @@ def search_options(self, item_name): return response.json() - def search_engine(self, search_query): + def search_engine(self, search_query, params): """ Search an item by URI. Use GLPI search engine passing parameter by URI. @@ -405,7 +405,7 @@ def search_engine(self, search_query): operations. """ new_uri = "%s/%s" % (self.uri, search_query) - response = self.request('GET', new_uri, accept_json=True) + response = self.request('GET', new_uri, params=params, accept_json=True) return response.json() @@ -702,14 +702,9 @@ def search_engine(self, item_name, criteria): flags=re.IGNORECASE) field_map[field_name] = int(field_id) - uri_query = "%s?" % item_name + params = {} for idx, c in enumerate(criteria['criteria']): - # build field argument - if idx == 0: - uri = "" - else: - uri = "&" if 'field' in c and c['field'] is not None: field_name = "" # if int given, use it directly @@ -722,25 +717,18 @@ def search_engine(self, item_name, criteria): raise GlpiInvalidArgument( 'Cannot map field name "' + c['field'] + '" to ' + 'a field id for '+str(idx+1)+'. criterion '+str(c)) - uri = uri + "criteria[%d][field]=%d" % (idx, field_name) + params["criteria[%d][field]" % idx] = field_name else: raise GlpiInvalidArgument( 'Missing "field" parameter for ' + str(idx+1) + 'the criteria: ' + str(c)) # build value argument - if 'value' not in c or c['value'] is None: - uri = uri + "&criteria[%d][value]=" % (idx) - else: - uri = uri + "&criteria[%d][value]=%s" % (idx, c['value']) + params["criteria[%d][value]" % idx] = c.get('value', '') # build searchtype argument # -> optional! defaults to "contains" on the server if empty - if 'searchtype' in c and c['searchtype'] is not None: - uri = (uri + "&criteria[%d][searchtype]=%s".format(idx, - c['searchtype'])) - else: - uri = uri + "&criteria[%d][searchtype]=" % (idx) + params["criteria[%d][searchtype]" % idx] = c.get('searchtype', '') # link is optional for 1st criterion according to docs... # -> error if not present but more than one criterion @@ -748,18 +736,14 @@ def search_engine(self, item_name, criteria): raise GlpiInvalidArgument( 'Missing link type for '+str(idx+1)+'. criterion '+str(c)) elif 'link' in c: - uri = uri + "&criteria[%d][link]=%s" % (idx, c['link']) - - # add this criterion to the query - uri_query = uri_query + uri + params["criteria[%d][link]" % idx] = c.get('link', '') try: if not self.api_has_session(): self.init_api() self.update_uri('search') - # TODO: is this call correct? shouldn't this be search_engine()? - return self.api_rest.search_options(uri_query) + return self.api_rest.search_engine(item_name, params) except GlpiException as e: return {'{}'.format(e)} From 1662dacbe09e97b8e7fb7b62d24d7a73c48968e4 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Fri, 1 Nov 2019 16:40:25 +0100 Subject: [PATCH 2/5] search_engine: Allow to pass other parameters We simply pass through everything that isn't the "criteria" parameter. This allows for specifying the range an other parameters of the search --- glpi/glpi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/glpi/glpi.py b/glpi/glpi.py index 9ba50c8..623d258 100644 --- a/glpi/glpi.py +++ b/glpi/glpi.py @@ -672,7 +672,7 @@ def search(self, item_name, criteria): else: return {"message_error": "Unable to find a valid criteria."} - def search_engine(self, item_name, criteria): + def search_engine(self, item_name, params): """ Call GLPI's search engine syntax. @@ -702,9 +702,7 @@ def search_engine(self, item_name, criteria): flags=re.IGNORECASE) field_map[field_name] = int(field_id) - params = {} - - for idx, c in enumerate(criteria['criteria']): + for idx, c in enumerate(params['criteria']): if 'field' in c and c['field'] is not None: field_name = "" # if int given, use it directly @@ -738,6 +736,8 @@ def search_engine(self, item_name, criteria): elif 'link' in c: params["criteria[%d][link]" % idx] = c.get('link', '') + del params['criteria'] + try: if not self.api_has_session(): self.init_api() From 93b7e948db0b1968b97773bd6b771348a64abe63 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Fri, 1 Nov 2019 16:58:55 +0100 Subject: [PATCH 3/5] search_engine: Also map forcedisplay values to field IDs --- glpi/glpi.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/glpi/glpi.py b/glpi/glpi.py index 623d258..1be3620 100644 --- a/glpi/glpi.py +++ b/glpi/glpi.py @@ -690,6 +690,17 @@ def search_engine(self, item_name, params): GLPIs APIRest JSON formated with result of search in key 'data'. """ + def field_to_int(field, field_map): + # if int given, use it directly + if isinstance(field, int) or field.isdigit(): + return int(field) + # if name given, try to map to an int + elif field in field_map: + return field_map[field] + else: + raise GlpiInvalidArgument( + 'Cannot map field name "' + field + '" to a field id.') + # Receive the possible field ids for type item_name # -> to avoid wrong lookups, use uid of fields, but strip item type: # example: {"1": {"uid": "Computer.name"}} gets {"name": 1} @@ -704,18 +715,8 @@ def search_engine(self, item_name, params): for idx, c in enumerate(params['criteria']): if 'field' in c and c['field'] is not None: - field_name = "" - # if int given, use it directly - if isinstance(c['field'], int) or c['field'].isdigit(): - field_name = int(c['field']) - # if name given, try to map to an int - elif c['field'] in field_map: - field_name = field_map[c['field']] - else: - raise GlpiInvalidArgument( - 'Cannot map field name "' + c['field'] + '" to ' + - 'a field id for '+str(idx+1)+'. criterion '+str(c)) - params["criteria[%d][field]" % idx] = field_name + params["criteria[%d][field]" % idx] = field_to_int( + c['field'], field_map) else: raise GlpiInvalidArgument( 'Missing "field" parameter for ' + str(idx+1) + @@ -738,6 +739,12 @@ def search_engine(self, item_name, params): del params['criteria'] + if 'forcedisplay' in params: + for idx, field in enumerate(params['forcedisplay']): + params['forcedisplay[%d]' % idx] = field_to_int(field, field_map) + + del params['forcedisplay'] + try: if not self.api_has_session(): self.init_api() From 8a5ce52629fbc073547b8bc8c97a4c1afa25150b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Thu, 7 Nov 2019 11:34:32 +0100 Subject: [PATCH 4/5] search_engine: Support nested filter groups --- glpi/glpi.py | 65 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/glpi/glpi.py b/glpi/glpi.py index 1be3620..4419de6 100644 --- a/glpi/glpi.py +++ b/glpi/glpi.py @@ -713,29 +713,48 @@ def field_to_int(field, field_map): flags=re.IGNORECASE) field_map[field_name] = int(field_id) - for idx, c in enumerate(params['criteria']): - if 'field' in c and c['field'] is not None: - params["criteria[%d][field]" % idx] = field_to_int( - c['field'], field_map) - else: - raise GlpiInvalidArgument( - 'Missing "field" parameter for ' + str(idx+1) + - 'the criteria: ' + str(c)) - - # build value argument - params["criteria[%d][value]" % idx] = c.get('value', '') - - # build searchtype argument - # -> optional! defaults to "contains" on the server if empty - params["criteria[%d][searchtype]" % idx] = c.get('searchtype', '') - - # link is optional for 1st criterion according to docs... - # -> error if not present but more than one criterion - if 'link' not in c and idx > 0: - raise GlpiInvalidArgument( - 'Missing link type for '+str(idx+1)+'. criterion '+str(c)) - elif 'link' in c: - params["criteria[%d][link]" % idx] = c.get('link', '') + def build_params_from_criteria(criteria, prefix): + for idx, c in enumerate(criteria): + if 'criteria' in c: + yield from build_params_from_criteria( + c['criteria'], + '%s[%d][criteria]' % (prefix, idx), + ) + elif 'field' in c and c['field'] is not None: + yield ( + "%s[%d][field]" % (prefix, idx), + field_to_int(c['field'], field_map), + ) + else: + raise GlpiInvalidArgument( + 'Missing "field" parameter for ' + str(idx+1) + + 'the criteria: ' + str(c)) + + # build value argument + yield ( + "%s[%d][value]" % (prefix, idx), + c.get('value', ''), + ) + + # build searchtype argument + # -> optional! defaults to "contains" on the server if empty + yield ( + "%s[%d][searchtype]" % (prefix, idx), + c.get('searchtype', ''), + ) + + if 'link' in c: + yield ( + "%s[%d][link]" % (prefix, idx), + c['link'], + ) + elif idx > 0: + # link is optional for 1st criterion according to docs... + # -> error if not present but more than one criterion + raise GlpiInvalidArgument( + 'Missing link type for '+str(idx+1)+'. criterion '+str(c)) + + params.update(build_params_from_criteria(params['criteria'], 'criteria')) del params['criteria'] From 6244c73318de1b73e329b0b12a0b9249866f227f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Wed, 13 Nov 2019 09:45:39 +0100 Subject: [PATCH 5/5] Fix for Python 2.7 (no "yield from") --- glpi/glpi.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/glpi/glpi.py b/glpi/glpi.py index 4419de6..190bbdb 100644 --- a/glpi/glpi.py +++ b/glpi/glpi.py @@ -716,10 +716,11 @@ def field_to_int(field, field_map): def build_params_from_criteria(criteria, prefix): for idx, c in enumerate(criteria): if 'criteria' in c: - yield from build_params_from_criteria( + for param in build_params_from_criteria( c['criteria'], '%s[%d][criteria]' % (prefix, idx), - ) + ): + yield param elif 'field' in c and c['field'] is not None: yield ( "%s[%d][field]" % (prefix, idx),