diff --git a/ckanext/right_time_context/controller.py b/ckanext/right_time_context/controller.py index a599316..99bc607 100644 --- a/ckanext/right_time_context/controller.py +++ b/ckanext/right_time_context/controller.py @@ -21,7 +21,8 @@ # along with Orion Context Broker. If not, see http://www.gnu.org/licenses/. import json -from logging import getLogger +import logging + import os import urlparse @@ -31,180 +32,13 @@ import requests import six -from .plugin import NGSI_REG_FORMAT - -log = getLogger(__name__) +import ckanext.right_time_context.utils as utils -CHUNK_SIZE = 512 +log = logging.getLogger(__name__) class ProxyNGSIController(base.BaseController): - def _proxy_query_resource(self, resource, parsed_url, headers, verify=True): - - if parsed_url.path.lower().find('/v1/querycontext') != -1: - if resource.get("payload", "").strip() == "": - details = 'Please add a payload to complete the query.' - base.abort(409, detail=details) - - try: - json.loads(resource['payload']) - except json.JSONDecodeError: - details = "Payload field doesn't contain valid JSON data." - base.abort(409, detail=details) - - headers['Content-Type'] = "application/json" - r = requests.post(resource['url'], headers=headers, data=resource["payload"], stream=True, verify=verify) - - else: - r = requests.get(resource['url'], headers=headers, stream=True, verify=verify) - - return r - - def _proxy_registration_resource(self, resource, parsed_url, headers, verify=True): - path = parsed_url.path - - if path.endswith('/'): - path = path[:-1] - - path = path + '/v2/op/query' - attrs = [] - - if 'attrs_str' in resource and len(resource['attrs_str']): - attrs = resource['attrs_str'].split(',') - body = { - 'entities': [], - 'attrs': attrs - } - - # Include entity information - for entity in resource['entity']: - query_entity = { - 'type': entity['value'] - } - if 'isPattern' in entity and entity['isPattern'] == 'on': - query_entity['idPattern'] = entity['id'] - else: - query_entity['id'] = entity['id'] - - body['entities'].append(query_entity) - - # Parse expression to include georel information - if 'expression' in resource and len(resource['expression']): - # Separate expresion query strings - supported_expressions = ['georel', 'geometry', 'coords'] - parsed_expression = resource['expression'].split('&') - - expression = {} - for exp in parsed_expression: - parsed_exp = exp.split('=') - - if len(parsed_exp) != 2 or not parsed_exp[0] in supported_expressions: - base.abort(422, detail='The expression is not a valid one for NGSI Registration, only georel, geometry, and coords is supported') - else: - expression[parsed_exp[0]] = parsed_exp[1] - - body['expression'] = expression - - headers['Content-Type'] = 'application/json' - url = urlparse.urljoin(parsed_url.scheme + '://' + parsed_url.netloc, path) - response = requests.post(url, headers=headers, json=body, stream=True, verify=verify) - - return response - - def process_auth_credentials(self, resource, headers): - auth_method = resource.get('auth_type', 'none') - - if auth_method == "oauth2": - token = toolkit.c.usertoken['access_token'] - headers['Authorization'] = "Bearer %s" % token - elif auth_method == "x-auth-token-fiware": - # Deprecated method, Including OAuth2 token retrieved from the IdM - # on the Open Stack X-Auth-Token header - token = toolkit.c.usertoken['access_token'] - headers['X-Auth-Token'] = token - def proxy_ngsi_resource(self, resource_id): - # Chunked proxy for ngsi resources. - context = {'model': base.model, 'session': base.model.Session, 'user': base.c.user or base.c.author} - - log.info('Proxify resource {id}'.format(id=resource_id)) - resource = logic.get_action('resource_show')(context, {'id': resource_id}) - - headers = { - 'Accept': 'application/json' - } - - resource.setdefault('auth_type', 'none') - self.process_auth_credentials(resource, headers) - - if resource.get('tenant', '') != '': - headers['FIWARE-Service'] = resource['tenant'] - if resource.get('service_path', '') != '': - headers['FIWARE-ServicePath'] = resource['service_path'] - - url = resource['url'] - parsed_url = urlparse.urlsplit(url) - - if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc: - base.abort(409, detail='Invalid URL.') - - # Process verify configuration - verify_conf = os.environ.get('CKAN_RIGHT_TIME_CONTEXT_VERIFY_REQUESTS', toolkit.config.get('ckan.right_time_context.verify_requests')) - if verify_conf is None or (isinstance(verify_conf, six.string_types) and verify_conf.strip() == ""): - verify_conf = os.environ.get('CKAN_VERIFY_REQUESTS', toolkit.config.get('ckan.verify_requests')) - - if isinstance(verify_conf, six.string_types) and verify_conf.strip() != "": - compare_env = verify_conf.lower().strip() - if compare_env in ("true", "1", "on"): - verify = True - elif compare_env in ("false", "0", "off"): - verify = False - else: - verify = verify_conf - elif isinstance(verify_conf, bool): - verify = verify_conf - else: - verify = True - - # Make the request to the server - try: - if resource['format'].lower() == NGSI_REG_FORMAT: - r = self._proxy_registration_resource(resource, parsed_url, headers, verify=verify) - else: - r = self._proxy_query_resource(resource, parsed_url, headers, verify=verify) - - except requests.HTTPError: - details = 'Could not proxy ngsi_resource. We are working to resolve this issue as quickly as possible' - base.abort(409, detail=details) - except requests.ConnectionError: - details = 'Could not proxy ngsi_resource because a connection error occurred.' - base.abort(502, detail=details) - except requests.Timeout: - details = 'Could not proxy ngsi_resource because the connection timed out.' - base.abort(504, detail=details) - - if r.status_code == 401: - if resource.get('auth_type', 'none') != 'none': - details = 'ERROR 401 token expired. Retrieving new token, reload please.' - log.info(details) - toolkit.c.usertoken_refresh() - base.abort(409, detail=details) - elif resource.get('auth_type', 'none') == 'none': - details = 'Authentication requested by server, please check resource configuration.' - log.info(details) - base.abort(409, detail=details) - - elif r.status_code == 400: - response = r.json() - details = response['description'] - log.info(details) - base.abort(422, detail=details) - - else: - r.raise_for_status() - base.response.content_type = r.headers['content-type'] - base.response.charset = r.encoding + utils.proxy_ngsi_resource(resource_id) - for chunk in r.iter_content(chunk_size=CHUNK_SIZE): - base.response.body_file.write(chunk) diff --git a/ckanext/right_time_context/fanstatic/view_ngsi.min.js b/ckanext/right_time_context/fanstatic/view_ngsi.min.js deleted file mode 100644 index 4396fea..0000000 --- a/ckanext/right_time_context/fanstatic/view_ngsi.min.js +++ /dev/null @@ -1,35 +0,0 @@ -ckan.module('right_time_context',function(jQuery,_){ - return{ - options:{ - i18n:{error:_('An error occurred: %(text)s %(error)s')}, - parameters:{contentType:'application/json', - dataType:'json', - dataConverter:function(data){return JSON.stringify(data,null,2);}, - language:'json',type:'GET'} - }, - initialize:function(){ - var self=this; - var p; - p=this.options.parameters; - if(typeof(view_enable) == 'undefined'){ - view_enable = []; - view_enable[0] = true; - resource_url = preload_resource['url'] - } - if(view_enable[0]){ - jQuery.ajax(resource_url,{type:p.type,contentType:p.contentType,dataType:p.dataType,success:function(data,textStatus,jqXHR){ - data=p.dataConverter?p.dataConverter(data):data; - var highlighted; - if(p.language){highlighted=hljs.highlight(p.language,data,true).value;} - else{highlighted='
'+data+'
';} - self.el.html(highlighted); - }, - error:function(jqXHR,textStatus,errorThrown){ - if(textStatus=='error'&&jqXHR.responseText.length){self.el.html(jqXHR.responseText);} - else{self.el.html(self.i18n('error',{text:textStatus,error:errorThrown}));}}}); - } - else{ - self.el.html("\n\n\n"+view_enable[1]+"\n\n\n"); - } - - }};}); diff --git a/ckanext/right_time_context/fanstatic/webassets.yml b/ckanext/right_time_context/fanstatic/webassets.yml new file mode 100644 index 0000000..a0782a1 --- /dev/null +++ b/ckanext/right_time_context/fanstatic/webassets.yml @@ -0,0 +1,19 @@ +main-css: + output: ckanext-right_time_context/%(version)s_ngsi_icons.css + contents: + - vendor/bootstrap/css/bootstrap-responsive.css + - styles/github.css + - css/ngsi.css + - css/ngsi_icons.css + +main: + output: ckanext-right_time_context/%(version)s_ngsi_icons.js + extra: + preload: + - base/main + - ckanext-right_time_context/main-css + contents: + - vendor/highlight/highlight.pack.js + - vendor/dygraph/dygraph-combined.js + - view_ngsi.js + diff --git a/ckanext/right_time_context/plugin.py b/ckanext/right_time_context/plugin/__init__.py similarity index 82% rename from ckanext/right_time_context/plugin.py rename to ckanext/right_time_context/plugin/__init__.py index 5c65749..7994ac1 100644 --- a/ckanext/right_time_context/plugin.py +++ b/ckanext/right_time_context/plugin/__init__.py @@ -23,38 +23,35 @@ import logging from ckan.common import _, json -import ckan.plugins as p +from ckan import plugins as p import ckan.lib.helpers as h +from ckan.plugins import toolkit log = logging.getLogger(__name__) - +import ckanext.right_time_context.utils as utils NGSI_FORMAT = 'fiware-ngsi' NGSI_REG_FORMAT = 'fiware-ngsi-registry' +if toolkit.check_ckan_version("2.10.1"): + from ckanext.right_time_context.plugin.flask_plugin import MixinPlugin +else: + from ckanext.right_time_context.plugin.pylons_plugin import MixinPlugin + def check_query(resource): parsedurl = resource['url'].lower() - return parsedurl.find('/v2/entities') != -1 or parsedurl.find('/v1/querycontext') != -1 or parsedurl.find('/v1/contextentities/') != -1 + return parsedurl.find('/entities') != -1 or parsedurl.find('/querycontext') != -1 or parsedurl.find('/contextentities/') != -1 -class NgsiView(p.SingletonPlugin): +class NgsiView(MixinPlugin, p.SingletonPlugin): - p.implements(p.IRoutes, inherit=True) p.implements(p.IConfigurer, inherit=True) p.implements(p.IConfigurable, inherit=True) p.implements(p.IResourceView, inherit=True) p.implements(p.IResourceController, inherit=True) p.implements(p.ITemplateHelpers) - def before_map(self, m): - m.connect( - '/dataset/{id}/resource/{resource_id}/ngsiproxy', - controller='ckanext.right_time_context.controller:ProxyNGSIController', - action='proxy_ngsi_resource' - ) - return m - def get_helpers(self): def get_available_auth_methods(): @@ -74,24 +71,17 @@ def get_available_auth_methods(): "right_time_context_get_available_auth_methods": get_available_auth_methods, } - def get_proxified_ngsi_url(self, data_dict): - url = h.url_for( - action='proxy_ngsi_resource', - controller='ckanext.right_time_context.controller:ProxyNGSIController', - id=data_dict['package']['name'], - resource_id=data_dict['resource']['id'] - ) - log.info('Proxified url is {0}'.format(url)) - return url def configure(self, config): self.proxy_is_enabled = p.plugin_loaded('resource_proxy') self.oauth2_is_enabled = p.plugin_loaded('oauth2') def update_config(self, config): - p.toolkit.add_template_directory(config, 'templates') - p.toolkit.add_resource('fanstatic', 'right_time_context') - p.toolkit.add_public_directory(config, 'public') + p.toolkit.add_template_directory(config, '../templates') + p.toolkit.add_resource('../fanstatic', 'ckanext-right_time_context') + p.toolkit.add_public_directory(config, '../public') + + def info(self): return { @@ -132,7 +122,7 @@ def setup_template_variables(self, context, data_dict): h.flash_error(f_details, allow_html=False) view_enable = [False, details] elif format_lower == NGSI_FORMAT and not check_query(resource): - details = "

This is not a ContextBroker query, please check Orion Context Broker documentation


" + #details = "

This is not a ContextBroker query, please check Orion Context Broker documentation


" f_details = "This is not a ContextBroker query, please check Orion Context Broker documentation." h.flash_error(f_details, allow_html=False) view_enable = [False, details] @@ -143,7 +133,7 @@ def setup_template_variables(self, context, data_dict): view_enable = [False, details] else: # All checks passed - url = self.get_proxified_ngsi_url(data_dict) + url = utils.get_proxified_ngsi_url(data_dict) data_dict['resource']['url'] = url view_enable = [True, 'OK'] @@ -209,20 +199,37 @@ def remove_serialized(prefix): return serialized_resource + # CKAN < 2.10 hooks def before_create(self, context, resource): - return self._serialize_resource(resource) + return self.before_resource_create(context, resource) def after_create(self, context, resource): + return self.after_resource_create(context, resource) + + def before_update(self, context, current, resource): + return self.before_resource_update(context, current, resource) + + def after_update(self, context, resource): + return self.after_resource_update(context, resource) + + def before_show(self, resource): + return self.before_resource_show(resource) + + # CKAN >= 2.10 hooks + def before_resource_create(self, context, resource): + return self._serialize_resource(resource) + + def after_resource_create(self, context, resource): # Create entry in the NGSI registry pass - def before_update(self, context, current, resource): + def before_resource_update(self, context, current, resource): return self._serialize_resource(resource) - def after_update(self, context, resource): + def after_resource_update(self, context, resource): pass - def before_show(self, resource): + def before_resource_show(self, resource): # Deserialize resource information entities = [] @@ -242,3 +249,4 @@ def deserilize_handler(prefix): resource['entity'] = entities return resource + diff --git a/ckanext/right_time_context/plugin/flask_plugin.py b/ckanext/right_time_context/plugin/flask_plugin.py new file mode 100644 index 0000000..ab5d4fd --- /dev/null +++ b/ckanext/right_time_context/plugin/flask_plugin.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from ckan import plugins as p + +from ckanext.right_time_context.views import get_blueprints + + +class MixinPlugin(p.SingletonPlugin): + p.implements(p.IBlueprint) + + # IBlueprint + + def get_blueprint(self): + return get_blueprints() + diff --git a/ckanext/right_time_context/plugin/pylons_plugin.py b/ckanext/right_time_context/plugin/pylons_plugin.py new file mode 100644 index 0000000..9235038 --- /dev/null +++ b/ckanext/right_time_context/plugin/pylons_plugin.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ckan import plugins as p + + +class MixinPlugin(p.SingletonPlugin): + + p.implements(p.IRoutes, inherit=True) + + def before_map(self, m): + m.connect( + '/dataset/{id}/resource/{resource_id}/ngsiproxy', + controller='ckanext.right_time_context.controller:ProxyNGSIController', + action='proxy_ngsi_resource' + ) + return m diff --git a/ckanext/right_time_context/templates/base.html b/ckanext/right_time_context/templates/base.html index d3deaf8..2605207 100644 --- a/ckanext/right_time_context/templates/base.html +++ b/ckanext/right_time_context/templates/base.html @@ -2,5 +2,7 @@ {% block styles %} {{ super() }} - {% resource 'right_time_context/css/ngsi_icons.css' %} + {% set type = 'asset' if h.ckan_version().split('.')[1] | int >= 9 else 'resource' %} + {% include 'package/snippets/main_' ~ type ~ '.html' %} {% endblock %} + diff --git a/ckanext/right_time_context/templates/ngsi.html b/ckanext/right_time_context/templates/ngsi.html index 92cd492..ff4bd9e 100644 --- a/ckanext/right_time_context/templates/ngsi.html +++ b/ckanext/right_time_context/templates/ngsi.html @@ -1,4 +1,4 @@ -{% extends 'dataviewer/base.html' %} +{% extends 'base.html' %} {% block page %}
@@ -18,12 +18,5 @@ {% endblock %} -{% resource 'right_time_context/vendor/highlight/highlight.pack.js' %} -{% resource 'right_time_context/vendor/bootstrap/js/bootstrap.js' %} -{% resource 'right_time_context/vendor/dygraph/dygraph-combined.js' %} -{% resource 'right_time_context/view_ngsi.js' %} -{% resource 'right_time_context/vendor/bootstrap/css/bootstrap-responsive.css' %} -{% resource 'right_time_context/vendor/bootstrap/css/bootstrap.css' %} -{% resource 'right_time_context/css/ngsi.css' %} - {% endblock %} + diff --git a/ckanext/right_time_context/templates/package/resource_read.html b/ckanext/right_time_context/templates/package/resource_read.html index aeade8f..2e4cd4b 100644 --- a/ckanext/right_time_context/templates/package/resource_read.html +++ b/ckanext/right_time_context/templates/package/resource_read.html @@ -2,10 +2,12 @@ {% block resource_actions_inner %} {% if h.check_access('package_update', {'id':pkg.id }) %} -
  • {% link_for _('Manage'), controller='package', action='resource_edit', id=pkg.name, resource_id=res.id, class_='btn', icon='wrench' %}
  • +
  • {% link_for _('Edit resource'), named_route=pkg.type ~ '_resource.edit', id=pkg.name, resource_id=res.id, class_='btn btn-default', icon='pencil' %}
  • + {% block action_manage_inner %}{% endblock %} +
  • {% link_for _('Views'), named_route=pkg.type ~ '_resource.views', id=pkg.name, resource_id=res.id, class_='btn btn-default', icon='chart-bar' %} {% endif %} {% if res.url and h.is_url(res.url) %} - {% if res.url.lower().find('/querycontext')== -1 and res.url.lower().find('/contextentities/')== -1 and res.url.lower().find('/entities/')== -1 and res.format.lower().find('fiware-ngsi-registry') == -1 %} + {% if res.url.lower().find('/querycontext')== -1 and res.url.lower().find('/contextentities/')== -1 and res.url.lower().find('/entities/')== -1 and res.format.lower().find('fiware-ngsi-registry') == -1 and res.format.lower().find('fiware-ngsi') == -1 %}
  • {% if res.resource_type in ('listing', 'service') %} @@ -40,3 +42,4 @@

    {{ _('URL:') }} {{ res.url }}

    {% endif %} {% endblock %} + diff --git a/ckanext/right_time_context/templates/package/snippets/main_asset.html b/ckanext/right_time_context/templates/package/snippets/main_asset.html new file mode 100644 index 0000000..0156dc4 --- /dev/null +++ b/ckanext/right_time_context/templates/package/snippets/main_asset.html @@ -0,0 +1 @@ +{% asset 'ckanext-right_time_context/main' %} diff --git a/ckanext/right_time_context/templates/package/snippets/main_resource.html b/ckanext/right_time_context/templates/package/snippets/main_resource.html new file mode 100644 index 0000000..2668400 --- /dev/null +++ b/ckanext/right_time_context/templates/package/snippets/main_resource.html @@ -0,0 +1 @@ +{% resource 'ckanext-right_time_context/main' %} diff --git a/ckanext/right_time_context/templates/package/snippets/resource_form.html b/ckanext/right_time_context/templates/package/snippets/resource_form.html index b93fa48..a4d7d18 100644 --- a/ckanext/right_time_context/templates/package/snippets/resource_form.html +++ b/ckanext/right_time_context/templates/package/snippets/resource_form.html @@ -119,9 +119,9 @@ }; const updatePayloadField = function updatePayloadField() { - let url, field = $("#field-image-url"); + let url, field = $("#field-resource-url"); - if ($("#field-format").val() !== "fiware-ngsi" || field.val().trim() === "") { + if ($("#field-format").val() !== "fiware-ngsi") { $('.ngsiview-v1').addClass('hidden'); return; } @@ -155,9 +155,8 @@ if (e.val === "fiware-ngsi" || e.val === 'fiware-ngsi-registry') { $('.ngsiview-input').removeClass('hidden'); // Support the datastore plugin - $("#field-image-url").css("padding-right", "6px"); + $("#field-resource-url").css("padding-right", "6px"); $('.btn-remove-url').addClass('hidden'); - $("#field-image-upload + a + a")[0].click(); if (e.val === 'fiware-ngsi-registry') { // Activate registry specific fields @@ -168,7 +167,7 @@ } else { $('.ngsiview-input').addClass('hidden'); // Support the datastore plugin - $("#field-image-url").css("padding-right", ""); + $("#field-resource-url").css("padding-right", ""); $('.btn-remove-url').removeClass('hidden'); } }); @@ -185,16 +184,17 @@ // Support the datastore plugin setTimeout(function () { updatePayloadField(); - $("#field-image-url").css("padding-right", "6px"); + $("#field-resource-url").css("padding-right", "6px"); $('.btn-remove-url').addClass('hidden'); - $("#field-image-upload + a + a")[0].click(); + $("#field-resource-upload + a + a")[0].click(); }, 200); } setTimeout(function () { - $("#field-image-url").on("input", updatePayloadField); + $("#field-resource-url").on("input", updatePayloadField); }, 200); }); {% endblock basic_fields %} + diff --git a/ckanext/right_time_context/templates/package/snippets/resource_item.html b/ckanext/right_time_context/templates/package/snippets/resource_item.html index 457533f..fbcc132 100644 --- a/ckanext/right_time_context/templates/package/snippets/resource_item.html +++ b/ckanext/right_time_context/templates/package/snippets/resource_item.html @@ -1,25 +1,27 @@ {% ckan_extends %} {% block resource_item_explore_links %} +{% block explore_view %}
  • - + {% if res.has_views %} - + {{ _('Preview') }} {% else %} - + {{ _('More information') }} {% endif %}
  • + {% endblock explore_view %} {% if res.url and h.is_url(res.url) %} - {% if res.url.lower().find('/querycontext')== -1 and res.url.lower().find('/contextentities/')== -1 and res.url.lower().find('/entities/')== -1 and res.format.lower().find('fiware-ngsi-registry') == -1 %} + {% if res.url.lower().find('/querycontext')== -1 and res.url.lower().find('/contextentities/')== -1 and res.url.lower().find('/entities/')== -1 and res.format.lower().find('fiware-ngsi-registry') == -1 and res.format.lower().find('fiware-ngsi') == -1 %}
  • - + {% if res.has_views %} - + {{ _('Download') }} {% else %} - + {{ _('Go to resource') }} {% endif %} @@ -27,11 +29,9 @@ {% endif %} {% endif %} {% if can_edit %} -
  • - - - {{ _('Edit') }} - -
  • +
  • {% link_for _('Edit resource'), named_route=pkg.type ~ '_resource.edit', id=pkg.name, resource_id=res.id, class_='dropdown-item', icon='pencil' %}
  • + {% block resource_item_explore_inner scoped %}{% endblock %} +
  • {% link_for _('Views'), named_route=pkg.type ~ '_resource.views', id=pkg.name, resource_id=res.id, class_='dropdown-item', icon='chart-bar' %}
  • {% endif %} {% endblock %} + diff --git a/ckanext/right_time_context/tests/test_plugin.py b/ckanext/right_time_context/tests/test_plugin.py index bb40cf6..131f3dd 100644 --- a/ckanext/right_time_context/tests/test_plugin.py +++ b/ckanext/right_time_context/tests/test_plugin.py @@ -20,7 +20,11 @@ import json import unittest -from mock import ANY, DEFAULT, patch +try: + from unittest.mock import ANY, DEFAULT, patch +except ImportError: + from mock import ANY, DEFAULT, patch + from parameterized import parameterized from ckan.plugins.toolkit import ValidationError diff --git a/ckanext/right_time_context/tests/test_controller.py b/ckanext/right_time_context/tests/test_utils.py similarity index 84% rename from ckanext/right_time_context/tests/test_controller.py rename to ckanext/right_time_context/tests/test_utils.py index 7bd38e1..dc7ebe6 100644 --- a/ckanext/right_time_context/tests/test_controller.py +++ b/ckanext/right_time_context/tests/test_utils.py @@ -18,11 +18,13 @@ # along with ckanext-right_time_context. If not, see http://www.gnu.org/licenses/. import unittest - -from mock import ANY, DEFAULT, patch +try: + from unittest.mock import ANY, DEFAULT, patch +except ImportError: + from mock import ANY, DEFAULT, patch from parameterized import parameterized -from ckanext.right_time_context.controller import ProxyNGSIController +import ckanext.right_time_context.utils as utils class NgsiViewControllerTestCase(unittest.TestCase): @@ -48,7 +50,6 @@ class NgsiViewControllerTestCase(unittest.TestCase): @classmethod def setUpClass(cls): super(NgsiViewControllerTestCase, cls).setUpClass() - cls.controller = ProxyNGSIController() def _mock_response(self, req_method): body = '{"json": "body"}' @@ -67,7 +68,7 @@ def _mock_response(self, req_method): ({"service_path": "/a", 'format': 'fiware-ngsi'}, {"FIWARE-ServicePath": "/a"}), ({"tenant": "a", "service_path": "/a,/b", 'format': 'fiware-ngsi'}, {"FIWARE-Service": "a", "FIWARE-ServicePath": "/a,/b"}), ]) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_basic_request(self, resource, headers, base, logic, requests, toolkit, os): resource['url'] = "http://cb.example.org/v2/entites" logic.get_action('resource_show').return_value = resource @@ -85,7 +86,7 @@ def test_basic_request(self, resource, headers, base, logic, requests, toolkit, } expected_headers.update(headers) - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") requests.get.assert_called_with(resource['url'], headers=expected_headers, stream=True, verify=True) base.response.body_file.write.assert_called_with(body) @@ -103,7 +104,7 @@ def test_basic_request(self, resource, headers, base, logic, requests, toolkit, 'attrs': [], }, {}) ]) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_registration_request(self, resource, query, headers, base, logic, requests, toolkit, os): logic.get_action('resource_show').return_value = resource response, body = self._mock_response(requests.post()) @@ -121,13 +122,13 @@ def test_registration_request(self, resource, query, headers, base, logic, reque } expected_headers.update(headers) - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") url = resource['url'] + '/v2/op/query' requests.post.assert_called_with(url, headers=expected_headers, json=query, stream=True, verify=True) base.response.body_file.write.assert_called_with(body) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_invalid_expression(self, base, logic, requests, toolkit, os): resource = { 'format': 'fiware-ngsi-registry', @@ -146,10 +147,10 @@ def test_invalid_expression(self, base, logic, requests, toolkit, os): logic.get_action('resource_show').return_value = resource - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") base.abort.assert_called_with(422, detail='The expression is not a valid one for NGSI Registration, only georel, geometry, and coords is supported') - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_invalid_reg_query(self, base, logic, requests, toolkit, os): logic.get_action('resource_show').return_value = self.REGISTRY_RESOURCE response, body = self._mock_response(requests.post()) @@ -161,7 +162,7 @@ def test_invalid_reg_query(self, base, logic, requests, toolkit, os): response.status_code = 400 response.json.return_value = {'description': err_msg} - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") base.abort.assert_called_with(422, detail=err_msg) @parameterized.expand([ @@ -171,7 +172,7 @@ def test_invalid_reg_query(self, base, logic, requests, toolkit, os): ("ftp://example.com",), ("tatata:///da",), ]) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_invalid_url_request(self, url, base, logic, requests, toolkit, os=DEFAULT): resource = { 'url': url, @@ -180,7 +181,7 @@ def test_invalid_url_request(self, url, base, logic, requests, toolkit, os=DEFAU base.abort.side_effect = TypeError with self.assertRaises(TypeError): - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") base.abort.assert_called_with(409, detail=ANY) requests.get.assert_not_called() @@ -189,7 +190,7 @@ def test_invalid_url_request(self, url, base, logic, requests, toolkit, os=DEFAU (True,), (False,), ]) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_auth_required_request(self, auth_configured, base, logic, requests, toolkit, os): resource = { 'url': "http://cb.example.org/v2/entites", @@ -206,7 +207,7 @@ def test_auth_required_request(self, auth_configured, base, logic, requests, too } with self.assertRaises(TypeError): - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") base.abort.assert_called_once_with(409, detail=ANY) requests.get.assert_called_once_with(resource['url'], headers=ANY, stream=True, verify=True) @@ -220,7 +221,7 @@ def test_auth_required_request(self, auth_configured, base, logic, requests, too ("ConnectionError", 502), ("Timeout", 504), ]) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_auth_required_request(self, exception, status_code, base, logic, requests, toolkit, os): resource = { 'url': "http://cb.example.org/v2/entites", @@ -232,7 +233,7 @@ def test_auth_required_request(self, exception, status_code, base, logic, reques base.abort.side_effect = TypeError with self.assertRaises(TypeError): - self.controller.proxy_ngsi_resource("resource_id") + utils.proxy_ngsi_resource("resource_id") base.abort.assert_called_once_with(status_code, detail=ANY) requests.get.assert_called_once_with(resource['url'], headers=ANY, stream=True, verify=True) @@ -256,7 +257,7 @@ def test_auth_required_request(self, exception, status_code, base, logic, reques ({"CKAN_RIGHT_TIME_CONTEXT_VERIFY_REQUESTS": " "}, {"ckan.verify_requests": False}, False), ({"CKAN_VERIFY_REQUESTS": "/path/A/b"}, {"ckan.verify_requests": "path/2"}, "/path/A/b"), ]) - @patch.multiple("ckanext.right_time_context.controller", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) + @patch.multiple("ckanext.right_time_context.utils", base=DEFAULT, logic=DEFAULT, requests=DEFAULT, toolkit=DEFAULT, os=DEFAULT) def test_verify_requests(self, env, config, expected_value, base, logic, requests, toolkit, os): logic.get_action('resource_show').return_value = { 'url': "https://cb.example.org/v2/entites", @@ -265,6 +266,8 @@ def test_verify_requests(self, env, config, expected_value, base, logic, request os.environ = env toolkit.config = config - with patch.object(self.controller, '_proxy_query_resource') as query_mock: - self.controller.proxy_ngsi_resource("resource_id") + with patch.object(utils, 'proxy_query_resource') as query_mock: + utils.proxy_ngsi_resource("resource_id") query_mock.assert_called_once_with(ANY, ANY, ANY, verify=expected_value) + + diff --git a/ckanext/right_time_context/utils.py b/ckanext/right_time_context/utils.py new file mode 100644 index 0000000..75f7930 --- /dev/null +++ b/ckanext/right_time_context/utils.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- + +import json +import logging +from six.moves.urllib.parse import urlsplit, urljoin +import os + +import requests +import six + +import ckan.lib.base as base +import ckan.logic as logic + +from ckan.plugins import toolkit + + +log = logging.getLogger(__name__) + +CHUNK_SIZE = 512 + +NGSI_REG_FORMAT = 'fiware-ngsi-registry' + + +def proxy_query_resource(resource, parsed_url, headers, verify=True): + if parsed_url.path.lower().find('/querycontext') != -1: + if resource.get("payload", "").strip() == "": + details = 'Please add a payload to complete the query.' + base.abort(409, detail=details) + + try: + json.loads(resource['payload']) + except json.JSONDecodeError: + details = "Payload field doesn't contain valid JSON data." + base.abort(409, detail=details) + + headers['Content-Type'] = "application/json" + r = requests.post(resource['url'], headers=headers, data=resource["payload"], stream=True, verify=verify) + + else: + r = requests.get(resource['url'], headers=headers, stream=True, verify=verify) + + return r + + +def proxy_registration_resource(resource, parsed_url, headers, verify=True): + path = parsed_url.path + + if path.endswith('/'): + path = path[:-1] + path = path + '/v2/op/query' + attrs = [] + + if 'attrs_str' in resource and len(resource['attrs_str']): + attrs = resource['attrs_str'].split(',') + body = { + 'entities': [], + 'attrs': attrs + } + + # Include entity information + for entity in resource['entity']: + query_entity = { + 'type': entity['value'] + } + if 'isPattern' in entity and entity['isPattern'] == 'on': + query_entity['idPattern'] = entity['id'] + else: + query_entity['id'] = entity['id'] + + body['entities'].append(query_entity) + + # Parse expression to include georel information + if 'expression' in resource and len(resource['expression']): + # Separate expresion query strings + supported_expressions = ['georel', 'geometry', 'coords'] + parsed_expression = resource['expression'].split('&') + + expression = {} + for exp in parsed_expression: + parsed_exp = exp.split('=') + + if len(parsed_exp) != 2 or not parsed_exp[0] in supported_expressions: + base.abort(422, detail='The expression is not a valid one for NGSI Registration, only georel, geometry, and coords is supported') + else: + expression[parsed_exp[0]] = parsed_exp[1] + + body['expression'] = expression + + headers['Content-Type'] = 'application/json' + url = urljoin(parsed_url.scheme + '://' + parsed_url.netloc, path) + log.info('Url: {}'.format(url)) + response = requests.post(url, headers=headers, json=body, stream=True, verify=verify) + + return response + + +def process_auth_credentials(resource, headers): + auth_method = resource.get('auth_type', 'none') + + if auth_method == "oauth2": + token = toolkit.c.usertoken['access_token'] + headers['Authorization'] = "Bearer %s" % token + elif auth_method == "x-auth-token-fiware": + # Deprecated method, Including OAuth2 token retrieved from the IdM + # on the Open Stack X-Auth-Token header + token = toolkit.c.usertoken['access_token'] + headers['X-Auth-Token'] = token + + +def proxy_ngsi_resource(resource_id): + # Chunked proxy for ngsi resources. + context = {'user': toolkit.c.user or toolkit.c.author} + + log.info('Proxify resource {id}'.format(id=resource_id)) + resource = logic.get_action('resource_show')(context, {'id': resource_id}) + + headers = { + 'Accept': 'application/json' + } + + resource.setdefault('auth_type', 'none') + process_auth_credentials(resource, headers) + + if resource.get('tenant', '') != '': + headers['FIWARE-Service'] = resource['tenant'] + if resource.get('service_path', '') != '': + headers['FIWARE-ServicePath'] = resource['service_path'] + + url = resource['url'] + parsed_url = urlsplit(url) + + if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc: + base.abort(409, detail='Invalid URL.') + + # Process verify configuration + verify_conf = os.environ.get('CKAN_RIGHT_TIME_CONTEXT_VERIFY_REQUESTS', + toolkit.config.get('ckan.right_time_context.verify_requests')) + if verify_conf is None or (isinstance(verify_conf, six.string_types) and verify_conf.strip() == ""): + verify_conf = os.environ.get('CKAN_VERIFY_REQUESTS', toolkit.config.get('ckan.verify_requests')) + + if isinstance(verify_conf, six.string_types) and verify_conf.strip() != "": + compare_env = verify_conf.lower().strip() + if compare_env in ("true", "1", "on"): + verify = True + elif compare_env in ("false", "0", "off"): + verify = False + else: + verify = verify_conf + elif isinstance(verify_conf, bool): + verify = verify_conf + else: + verify = True + + # Make the request to the server + try: + if resource['format'].lower() == NGSI_REG_FORMAT: + r = proxy_registration_resource(resource, parsed_url, headers, verify=verify) + else: + r = proxy_query_resource(resource, parsed_url, headers, verify=verify) + + except requests.HTTPError: + details = 'Could not proxy ngsi_resource. We are working to resolve this issue as quickly as possible' + base.abort(409, detail=details) + except requests.ConnectionError: + details = 'Could not proxy ngsi_resource because a connection error occurred.' + base.abort(502, detail=details) + except requests.Timeout: + details = 'Could not proxy ngsi_resource because the connection timed out.' + base.abort(504, detail=details) + + if r.status_code == 401: + if resource.get('auth_type', 'none') != 'none': + details = 'ERROR 401 token expired. Retrieving new token, reload please.' + log.info(details) + toolkit.c.usertoken_refresh() + base.abort(409, detail=details) + elif resource.get('auth_type', 'none') == 'none': + details = 'Authentication requested by server, please check resource configuration.' + log.info(details) + base.abort(409, detail=details) + + elif r.status_code == 400: + response = r.json() + details = response['description'] + log.info(details) + base.abort(422, detail=details) + + else: + r.raise_for_status() + if toolkit.check_ckan_version("2.10.1"): + from flask import make_response + response = make_response() + else: + response = toolkit.response + response.content_type = r.headers['content-type'] + response.charset = r.encoding + #base.response.content_type = r.headers['content-type'] + #base.response.charset = r.encoding + + for chunk in r.iter_content(chunk_size=CHUNK_SIZE): + if toolkit.check_ckan_version("2.10.1"): + response.data += chunk + else: + response.body_file.write(chunk) + #base.response.body_file.write(chunk) + return response + +def get_proxified_ngsi_url(data_dict): + log.info('in plugin utils get_proxified_ngsi_url data_dict {0}'.format(data_dict)) + url = toolkit.url_for( + action='proxy_ngsi', + controller='ngsiproxy', + id=data_dict['package']['name'], + resource_id=data_dict['resource']['id'] + ) + log.info('Proxified url is {0}'.format(url)) + return url + diff --git a/ckanext/right_time_context/views.py b/ckanext/right_time_context/views.py new file mode 100644 index 0000000..052f802 --- /dev/null +++ b/ckanext/right_time_context/views.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +import logging + +from flask import Blueprint + +import ckan.lib.base as base + +from ckan import plugins as p +from ckan.plugins import toolkit + +import ckanext.right_time_context.utils as utils + +log = logging.getLogger(__name__) +ngsiproxy = Blueprint("ngsiproxy", __name__) + + +def proxy_ngsi(id, resource_id): + return utils.proxy_ngsi_resource(resource_id) + + +def get_blueprints(): + return [ngsiproxy] + + +ngsiproxy.add_url_rule( + "/dataset//resource//ngsiproxy", + view_func=proxy_ngsi, +) diff --git a/setup.py b/setup.py index 5b55c7f..a79e00e 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,8 @@ def read(fname): 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.10', ], packages=find_packages(exclude=['contrib', 'docs', 'tests*']), install_requires=[],