From 39fa903d85ff94b58a5e40b28c7340195a7e5f49 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Fri, 8 Mar 2019 12:08:19 +0100
Subject: [PATCH 3/3] iparapheur: resolv no basic_authentication on w3.org
 (#31274)

---
 passerelle/contrib/iparapheur/models.py |  12 +--
 passerelle/utils/__init__.py            |  20 ++++-
 tests/test_iparapheur.py                | 115 +++++++++++++++++++++++-
 3 files changed, 140 insertions(+), 7 deletions(-)

diff --git a/passerelle/contrib/iparapheur/models.py b/passerelle/contrib/iparapheur/models.py
index 9a81eec..dcd806d 100644
--- a/passerelle/contrib/iparapheur/models.py
+++ b/passerelle/contrib/iparapheur/models.py
@@ -85,6 +85,8 @@ class IParapheur(BaseResource, HTTPResource):
             raise APIError('ServiceError: %s' % exc)
         except TransportError as exc:
             raise APIError('Transport Error: %s' % exc)
+        except TypeError as exc:
+            raise APIError('Type Error: %s' % exc)
         return result
 
     @endpoint(perm='can_access')
@@ -142,12 +144,12 @@ class IParapheur(BaseResource, HTTPResource):
                       }
         if email:
             parameters['EmailEmetteur'] = email
-        rep = soap_client.service.CreerDossier(**parameters)
-        if not rep or not rep.MessageRetour:
+        resp = soap_client.service.CreerDossier(**parameters)
+        if not resp or not resp.MessageRetour:
             raise FileError('unknown error, no response')
-        if rep.MessageRetour.codeRetour == 'KO':
-            raise FileError(rep.MessageRetour.message)
-        return {'data': {'RecordId': rep.DossierID, 'message': rep.MessageRetour.message}}
+        if resp.MessageRetour.codeRetour == 'KO':
+            raise FileError(resp.MessageRetour.message)
+        return {'data': {'RecordId': resp.DossierID, 'message': resp.MessageRetour.message}}
 
     @endpoint(perm='can_access', name='get-file', pattern='(?P<file_id>[\w-]+)')
     def get_file(self, request, file_id, appendix=None):
diff --git a/passerelle/utils/__init__.py b/passerelle/utils/__init__.py
index 9091ad4..0f09130 100644
--- a/passerelle/utils/__init__.py
+++ b/passerelle/utils/__init__.py
@@ -24,6 +24,7 @@ import warnings
 from requests import Session as RequestSession, Response as RequestResponse
 from requests.structures import CaseInsensitiveDict
 from urllib3.exceptions import InsecureRequestWarning
+from django.utils.six.moves.urllib import parse as urlparse
 
 from django.conf import settings
 from django.core.cache import cache
@@ -288,14 +289,31 @@ class Request(RequestSession):
         log_http_request(self.logger, request=request, response=response, exception=exception, error_log=error_log)
 
 
+class SOAPTransport(Transport):
+    """Wrapper around zeep.Transport
+
+    disable basic_authentication hosts unrelated to wsdl's endpoints
+    """
+    def __init__(self, wsdl_url, **kwargs):
+        self.wsdl_host = urlparse.urlparse(wsdl_url).netloc
+        super(SOAPTransport, self).__init__(**kwargs)
+
+    def _load_remote_data(self, url):
+        if urlparse.urlparse(url).netloc != self.wsdl_host:
+            response = self.session.get(url, timeout=self.load_timeout, auth=None, cert=None)
+            response.raise_for_status()
+            return response.content
+        return super(SOAPTransport, self)._load_remote_data(url)
+
+
 class SOAPClient(Client):
     """Wrapper around zeep.Client
 
     resource muste have a wsdl_url and a requests attribute
     """
     def __init__(self, resource, **kwargs):
-        transport = Transport(session=resource.requests, cache=InMemoryCache())
         wsdl_url = kwargs.pop('wsdl_url', None) or resource.wsdl_url
+        transport = SOAPTransport(wsdl_url, session=resource.requests, cache=InMemoryCache())
         super(SOAPClient, self).__init__(wsdl_url, transport=transport, **kwargs)
 
 
diff --git a/tests/test_iparapheur.py b/tests/test_iparapheur.py
index 51f433c..8a124ee 100644
--- a/tests/test_iparapheur.py
+++ b/tests/test_iparapheur.py
@@ -54,8 +54,9 @@ def iph_mocked_get(url, params=None, **kwargs):
     if url == 'https://secure-iparapheur.demonstrations.adullact.org:443/ws-iparapheur?wsdl':
         target_file = wsdl_file()
 
-    # simulate second GET call (nested <xsd:import> into wsdl fil)
+    # simulate second GET call (nested <xsd:import> into wsdl file)
     elif url == 'http://www.w3.org/2005/05/xmlmime':
+        assert kwargs['auth'] is None  # no basic_authenticton on wsdl' imports
         target_file = xmlmime()
 
     else:
@@ -98,6 +99,7 @@ def test_create_file(mocked_post, mocked_get, app, conn):
     typ = 'Courrier'
     subtyp = 'maire'
     visibility = 'SERVICE'
+    email = 'alice.boniadetos@arf.pt'
     response = Response()
     response.status_code = 200
 
@@ -155,6 +157,32 @@ def test_create_file(mocked_post, mocked_get, app, conn):
     assert 'zeep.exceptions.TransportError' in resp.json['err_class']
     assert 'Server returned HTTP status 200 (<nada>)' in resp.json['err_desc']
 
+    # Unknown value for "visibility"
+    data = {'type': typ, 'subtype': subtyp, 'visibility': 'UNKNOWN_VISIBILITY',
+            'title': title, 'data': base64_data, 'content-type':'application/pdf'}
+    url = reverse('generic-endpoint', kwargs={'connector': 'iparapheur',
+                                    'endpoint': 'create-file', 'slug': conn.slug})
+    url += '?apikey=%s' % API_KEY
+    resp = app.post_json(url, params=data, status=500)
+    assert resp.json['err'] == 1
+    assert 'FileError' in resp.json['err_class']
+
+    # OK, providing email
+    data = {'type': typ, 'subtype': subtyp, 'visibility': visibility,
+            'title': title, 'data': base64_data, 'content-type':'application/pdf',
+            'email': email}
+    url = reverse('generic-endpoint', kwargs={'connector': 'iparapheur',
+                                    'endpoint': 'create-file', 'slug': conn.slug})
+    url += '?apikey=%s' % API_KEY
+    soap_response = """<nada>"""
+    response._content = soap_response
+    mocked_post.return_value = response
+    resp = app.post_json(url, params=data, status=500)
+    assert (BASE_URL,) == mocked_post.call_args[0]
+    assert resp.json['err'] == 1
+    assert 'zeep.exceptions.TransportError' in resp.json['err_class']
+    assert 'Server returned HTTP status 200 (<nada>)' in resp.json['err_desc']
+
 @mock.patch('passerelle.utils.Request.get', side_effect=iph_mocked_get)
 @mock.patch('passerelle.utils.Request.post')
 def test_files(mocked_post, mocked_get, app, conn):
@@ -175,6 +203,18 @@ def test_files(mocked_post, mocked_get, app, conn):
         assert item['id']
         assert item['timestamp']
 
+    # same call providing parameter
+    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><RechercherDossiersResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"><LogDossier><timestamp>2015-10-29T15:42:08.732+01:00</timestamp><nom>test</nom><status>Lu</status><annotation></annotation><accessible>KO</accessible></LogDossier></RechercherDossiersResponse></S:Body></S:Envelope>"""
+    response._content = soap_response
+    mocked_post.return_value = response
+    url += '&status=Lu'
+    resp = app.get(url)
+    assert len(resp.json['data']) == 1
+    for item in resp.json['data']:
+        assert item['status']
+        assert item['id']
+        assert item['timestamp']
+
 @mock.patch('passerelle.utils.Request.get', side_effect=iph_mocked_get)
 @mock.patch('passerelle.utils.Request.post')
 def test_get_file_status(mocked_post, mocked_get, app, conn):
@@ -428,3 +468,76 @@ def test_call_wsdl_connectionerror(mocked_get, app, conn):
     assert resp.json['err'] == 1
     assert resp.json['data'] is None
     assert 'mocked error' in resp.json['err_desc']
+
+@mock.patch('passerelle.utils.Request.get')
+@mock.patch('zeep.Transport._load_remote_data')
+@mock.patch('passerelle.utils.Request.post')
+def test_no_auth_on_wsdl_imports(mocked_post, mocked_load, mocked_get, app, conn):
+    """
+    While getting wsdl:
+    - zeep.Transport._load_remote_data is call on basic_authentication (get wsdl file)
+    - passerelle.utils.Request.get is call on no basic_authenticaion (get xmlmime import's file)
+
+    Notice: this also test the 'echo' SOAP service
+    """
+    response_xmlmime, response_post = Response(), Response()
+    response_xmlmime.status_code, response_post.status_code = 200, 200
+    response_xmlmime._content=file(xmlmime()).read()
+    response_post._content = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><echoResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime">[publik_test] m'a dit: &quot;ping&quot;!</echoResponse></S:Body></S:Envelope>
+"""
+    mocked_load.return_value = file(wsdl_file()).read()
+    mocked_get.return_value = response_xmlmime
+    mocked_post.return_value = response_post
+    url = reverse('generic-endpoint', kwargs={'connector': 'iparapheur',
+                    'endpoint': 'ping', 'slug': conn.slug})
+
+    resp = app.get(url, status=403)
+    url += '?apikey=%s' % API_KEY
+    resp = app.get(url)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == "[publik_test] m'a dit: \"ping\"!"
+
+@mock.patch('passerelle.utils.Request.get', side_effect=iph_mocked_get)
+@mock.patch('passerelle.utils.Request.post')
+def test_types(mocked_post, mocked_get, app, conn):
+    """test GetListeTypesRequest SOAP service"""
+    response = Response()
+    response.status_code = 200
+
+    # empty list of types as response
+    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><GetListeTypesResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"/></S:Body></S:Envelope>"""
+
+    response._content = soap_response
+    mocked_post.return_value = response
+    url = reverse('generic-endpoint', kwargs={'slug': conn.slug, 'connector': 'iparapheur',
+                          'endpoint': 'types'})
+    url += '?apikey=%s' % API_KEY
+    resp = app.get(url)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == []
+
+@mock.patch('passerelle.utils.Request.get', side_effect=iph_mocked_get)
+@mock.patch('passerelle.utils.Request.post')
+def test_subtypes(mocked_post, mocked_get, app, conn):
+    """test GetListeSousTypes SOAP service"""
+    response = Response()
+    response.status_code = 200
+
+    # error: no parameter provided
+    url = reverse('generic-endpoint', kwargs={'slug': conn.slug, 'connector': 'iparapheur',
+                          'endpoint': 'subtypes'})
+    resp = app.get(url, status=403)
+    url += '?apikey=%s' % API_KEY
+    resp = app.get(url)
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == "Type Error: TypeTechnique() takes exactly 1 argument (0 given). Simple types expect only a single value argument"
+
+    # providing a type as parameter
+    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><GetListeTypesResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"/></S:Body></S:Envelope>"""
+
+    response._content = soap_response  # empty list of subtypes as response
+    mocked_post.return_value = response
+    url += '&type=my_unknown_type'
+    resp = app.get(url)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == []
-- 
2.20.1

