From 65409f2070a326185cc6106518e31621b0f8b15c Mon Sep 17 00:00:00 2001 From: Emmanuel Cazenave Date: Tue, 30 May 2023 16:30:46 +0200 Subject: [PATCH] sne: start connector (#77933) --- debian/control | 2 + passerelle/apps/sne/__init__.py | 0 .../apps/sne/migrations/0001_initial.py | 75 +++++++++ passerelle/apps/sne/migrations/__init__.py | 0 passerelle/apps/sne/models.py | 78 +++++++++ passerelle/settings.py | 1 + setup.py | 2 + .../data/sne/DemandeLogementImplService.wsdl | 76 +++++++++ tests/data/sne/DemandeLogementImplService.xsd | 154 ++++++++++++++++++ .../data/sne/DemandeLogementImplService1.wsdl | 72 ++++++++ tests/data/sne/cert.pem | 74 +++++++++ tests/data/sne/response_does_not_exist | 7 + tests/data/sne/response_ok | 13 ++ tests/data/sne/xmlmime.xsd | 49 ++++++ tests/test_sne.py | 97 +++++++++++ 15 files changed, 700 insertions(+) create mode 100644 passerelle/apps/sne/__init__.py create mode 100644 passerelle/apps/sne/migrations/0001_initial.py create mode 100644 passerelle/apps/sne/migrations/__init__.py create mode 100644 passerelle/apps/sne/models.py create mode 100644 tests/data/sne/DemandeLogementImplService.wsdl create mode 100644 tests/data/sne/DemandeLogementImplService.xsd create mode 100644 tests/data/sne/DemandeLogementImplService1.wsdl create mode 100644 tests/data/sne/cert.pem create mode 100644 tests/data/sne/response_does_not_exist create mode 100644 tests/data/sne/response_ok create mode 100644 tests/data/sne/xmlmime.xsd create mode 100644 tests/test_sne.py diff --git a/debian/control b/debian/control index b8ae6b3f..a3f92e14 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Depends: ghostscript, pdftk, poppler-utils, python3-cmislib, + python3-cryptography, python3-dateutil, python3-distutils, python3-django (>= 2:3.2), @@ -43,6 +44,7 @@ Depends: ghostscript, python3-uwsgidecorators, python3-vobject, python3-xmlschema, + python3-xmltodict, python3-zeep (>= 3.2), ${misc:Depends}, ${python3:Depends}, diff --git a/passerelle/apps/sne/__init__.py b/passerelle/apps/sne/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/passerelle/apps/sne/migrations/0001_initial.py b/passerelle/apps/sne/migrations/0001_initial.py new file mode 100644 index 00000000..14c911be --- /dev/null +++ b/passerelle/apps/sne/migrations/0001_initial.py @@ -0,0 +1,75 @@ +# Generated by Django 3.2.18 on 2023-05-31 08:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ('base', '0030_resourcelog_base_resour_appname_298cbc_idx'), + ] + + operations = [ + migrations.CreateModel( + name='SNE', + fields=[ + ( + 'id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ('title', models.CharField(max_length=50, verbose_name='Title')), + ('slug', models.SlugField(unique=True, verbose_name='Identifier')), + ('description', models.TextField(verbose_name='Description')), + ( + 'basic_auth_username', + models.CharField( + blank=True, max_length=128, verbose_name='Basic authentication username' + ), + ), + ( + 'basic_auth_password', + models.CharField( + blank=True, max_length=128, verbose_name='Basic authentication password' + ), + ), + ( + 'client_certificate', + models.FileField( + blank=True, null=True, upload_to='', verbose_name='TLS client certificate' + ), + ), + ( + 'trusted_certificate_authorities', + models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'), + ), + ( + 'verify_cert', + models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'), + ), + ( + 'http_proxy', + models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'), + ), + ( + 'wsdl_url', + models.URLField( + help_text='URL of the WSDL file', max_length=400, verbose_name='WSDL URL' + ), + ), + ( + 'certificate_name', + models.CharField(max_length=128, verbose_name='SOAP client certificate name'), + ), + ( + 'users', + models.ManyToManyField( + blank=True, related_name='_sne_sne_users_+', related_query_name='+', to='base.ApiUser' + ), + ), + ], + options={ + 'verbose_name': 'SNE', + }, + ), + ] diff --git a/passerelle/apps/sne/migrations/__init__.py b/passerelle/apps/sne/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/passerelle/apps/sne/models.py b/passerelle/apps/sne/models.py new file mode 100644 index 00000000..0f1633b1 --- /dev/null +++ b/passerelle/apps/sne/models.py @@ -0,0 +1,78 @@ +# passerelle - uniform access to multiple data sources and services +# Copyright (C) 2023 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import xmltodict +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from passerelle.base.models import BaseResource, HTTPResource +from passerelle.utils.api import endpoint + + +class SNE(BaseResource, HTTPResource): + wsdl_url = models.URLField( + max_length=400, verbose_name=_('WSDL URL'), help_text=_('URL of the WSDL file') + ) + certificate_name = models.CharField(max_length=128, verbose_name=_('Client certificate name')) + category = _('Business Process Connectors') + + class Meta: + verbose_name = _('SNE') + + @classmethod + def get_manager_form_class(cls, **kwargs): + form_class = super().get_manager_form_class(**kwargs) + form_class.base_fields['client_certificate'].required = True + return form_class + + @property + def cert_public_bytes(self): + with self.client_certificate.open('rb') as f: + certs = x509.load_pem_x509_certificates(f.read()) + cert = certs[0] + return cert.public_bytes(encoding=serialization.Encoding.PEM) + + def check_status(self): + response = self.requests.get(self.wsdl_url) + response.raise_for_status() + + @endpoint( + name='get-demande-logement', + description=_('Get informations on housing demand'), + parameters={ + 'demand_id': { + 'example_value': '1', + } + }, + ) + def get_demande_logement(self, request, demand_id, **kwargs): + client = self.soap_client(wsdl_url=self.wsdl_url, api_error=True) + cert_type = client.get_type('{http://ws.metier.nuu.application.i2/}base64Binary') + cert = cert_type(_value_1=self.cert_public_bytes) + res = client.service.getDemandeLogement( + numUnique=demand_id, nomCertificat=self.certificate_name, certificat=cert + ) + namespaces = { + 'http://nuu.application.i2/': None, + } + return { + 'data': xmltodict.parse( + res['fichierDemande']['_value_1'], process_namespaces=True, namespaces=namespaces + ) + } diff --git a/passerelle/settings.py b/passerelle/settings.py index a45d953c..1223c7de 100644 --- a/passerelle/settings.py +++ b/passerelle/settings.py @@ -179,6 +179,7 @@ INSTALLED_APPS = ( 'passerelle.apps.signal_arretes', 'passerelle.apps.sivin', 'passerelle.apps.smsfactor', + 'passerelle.apps.sne', 'passerelle.apps.soap', 'passerelle.apps.solis', 'passerelle.apps.twilio', diff --git a/setup.py b/setup.py index 8b0b0ca2..a2f5d75e 100755 --- a/setup.py +++ b/setup.py @@ -172,6 +172,8 @@ setup( 'python-ldap', 'pyOpenSSL', 'roman', + 'cryptography', + 'xmltodict', ], cmdclass={ 'build': build, diff --git a/tests/data/sne/DemandeLogementImplService.wsdl b/tests/data/sne/DemandeLogementImplService.wsdl new file mode 100644 index 00000000..3839b561 --- /dev/null +++ b/tests/data/sne/DemandeLogementImplService.wsdl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/sne/DemandeLogementImplService.xsd b/tests/data/sne/DemandeLogementImplService.xsd new file mode 100644 index 00000000..0cb9bff0 --- /dev/null +++ b/tests/data/sne/DemandeLogementImplService.xsd @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/sne/DemandeLogementImplService1.wsdl b/tests/data/sne/DemandeLogementImplService1.wsdl new file mode 100644 index 00000000..c0cd96ba --- /dev/null +++ b/tests/data/sne/DemandeLogementImplService1.wsdl @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/sne/cert.pem b/tests/data/sne/cert.pem new file mode 100644 index 00000000..b2578548 --- /dev/null +++ b/tests/data/sne/cert.pem @@ -0,0 +1,74 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 4c:aa:29:4f:be:82:16:5b:15:70:8e:c5:02:29:5a:60:b2:0b:cf:4a + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = localhost.entrouvert.org + Validity + Not Before: Dec 5 16:59:24 2018 GMT + Not After : Dec 2 16:59:24 2028 GMT + Subject: CN = localhost.entrouvert.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:e3:1b:0b:9f:6e:72:8a:db:5b:8e:4e:5b:70:ab: + d8:d8:8f:33:3e:2b:cb:e9:85:a7:d8:c2:62:99:a4: + 10:48:dc:c2:c3:ff:0f:3a:65:4f:2e:63:08:6f:79: + 5c:66:3c:9d:15:16:9d:f9:14:f1:65:f9:ee:bc:20: + 89:ae:71:ab:1b:b8:4e:b0:93:5e:9d:32:cb:53:4b: + a0:37:fc:90:d7:ce:bd:70:cd:3b:6b:db:63:4b:32: + fb:34:2c:fd:1f:53:48:2b:5f:90:70:1b:13:13:17: + 3b:ed:d3:96:a7:05:88:f5:38:ea:61:84:4b:fb:37: + b1:de:69:7c:71:da:9c:43:b0:50:51:0c:40:bc:0e: + 14:53:ad:c3:33:61:be:e4:ed:dd:13:b9:7c:ba:fc: + 81:51:4d:e4:5b:fb:21:1f:28:5f:c1:e5:8d:5c:ef: + 08:5d:72:23:bc:37:cf:62:43:ac:8d:ce:9e:9f:69: + 05:32:bf:ac:10:d4:94:2a:07:c3:2a:3c:ff:53:3a: + b2:4b:b2:c1:6f:8b:64:24:02:d9:33:c4:0f:e7:a5: + 5e:40:01:b5:25:91:76:27:6f:f2:d5:1a:81:7c:81: + 91:20:49:18:a1:a1:dc:8f:a5:00:4d:96:b5:c5:c6: + 63:32:e6:cb:cf:7a:2d:44:d6:1c:45:72:89:31:85: + 86:6d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Subject Alternative Name: + DNS:localhost.entrouvert.org + Signature Algorithm: sha256WithRSAEncryption + 15:53:da:bc:16:a1:98:88:e0:9d:38:ea:e9:96:c5:c5:74:cd: + 25:6d:13:ae:a3:7c:a2:56:ca:27:a4:9f:c1:65:64:d0:8f:29: + c7:b3:ce:7a:41:5a:5d:df:9e:82:c1:49:95:66:32:1f:da:b8: + 1e:42:a8:b5:d7:51:61:8a:d6:a1:77:0f:8a:87:4d:7d:46:be: + 6d:19:e1:d9:66:25:da:b1:06:31:6e:5d:6c:17:ff:31:80:83: + b6:ce:bc:73:74:a8:03:b2:48:60:9f:30:d1:06:46:02:48:a3: + 28:db:55:30:bd:3f:16:b2:ee:25:66:df:14:10:16:1f:c2:41: + e3:a0:5c:02:0f:fc:7e:56:cc:1c:05:0b:cd:5d:c5:d6:0f:ef: + d0:0d:c5:1b:76:3f:f5:f5:6b:fa:53:79:aa:cf:8d:07:d3:57: + 20:c7:5f:e7:05:eb:98:94:18:46:3a:2d:c8:e8:d4:3b:ac:93: + 16:a0:c5:be:a1:3b:0c:4a:4a:40:3e:61:9e:fa:89:a1:70:b3: + cd:84:12:d4:38:c7:d6:a4:91:73:9b:c2:f0:8e:d8:94:9a:30: + 09:f9:c8:f4:cb:04:20:24:ab:b2:4e:f9:0e:14:f4:f7:56:89: + 0b:65:d7:f7:7a:38:ce:17:cd:c6:63:b9:2a:3d:bc:84:ff:3b: + 18:78:0d:22 +-----BEGIN CERTIFICATE----- +MIIDBjCCAe6gAwIBAgIUTKopT76CFlsVcI7FAilaYLILz0owDQYJKoZIhvcNAQEL +BQAwIzEhMB8GA1UEAwwYbG9jYWxob3N0LmVudHJvdXZlcnQub3JnMB4XDTE4MTIw +NTE2NTkyNFoXDTI4MTIwMjE2NTkyNFowIzEhMB8GA1UEAwwYbG9jYWxob3N0LmVu +dHJvdXZlcnQub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4xsL +n25yittbjk5bcKvY2I8zPivL6YWn2MJimaQQSNzCw/8POmVPLmMIb3lcZjydFRad ++RTxZfnuvCCJrnGrG7hOsJNenTLLU0ugN/yQ1869cM07a9tjSzL7NCz9H1NIK1+Q +cBsTExc77dOWpwWI9TjqYYRL+zex3ml8cdqcQ7BQUQxAvA4UU63DM2G+5O3dE7l8 +uvyBUU3kW/shHyhfweWNXO8IXXIjvDfPYkOsjc6en2kFMr+sENSUKgfDKjz/Uzqy +S7LBb4tkJALZM8QP56VeQAG1JZF2J2/y1RqBfIGRIEkYoaHcj6UATZa1xcZjMubL +z3otRNYcRXKJMYWGbQIDAQABozIwMDAJBgNVHRMEAjAAMCMGA1UdEQQcMBqCGGxv +Y2FsaG9zdC5lbnRyb3V2ZXJ0Lm9yZzANBgkqhkiG9w0BAQsFAAOCAQEAFVPavBah +mIjgnTjq6ZbFxXTNJW0TrqN8olbKJ6SfwWVk0I8px7POekFaXd+egsFJlWYyH9q4 +HkKotddRYYrWoXcPiodNfUa+bRnh2WYl2rEGMW5dbBf/MYCDts68c3SoA7JIYJ8w +0QZGAkijKNtVML0/FrLuJWbfFBAWH8JB46BcAg/8flbMHAULzV3F1g/v0A3FG3Y/ +9fVr+lN5qs+NB9NXIMdf5wXrmJQYRjotyOjUO6yTFqDFvqE7DEpKQD5hnvqJoXCz +zYQS1DjH1qSRc5vC8I7YlJowCfnI9MsEICSrsk75DhT091aJC2XX93o4zhfNxmO5 +Kj28hP87GHgNIg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/data/sne/response_does_not_exist b/tests/data/sne/response_does_not_exist new file mode 100644 index 00000000..52d1c37d --- /dev/null +++ b/tests/data/sne/response_does_not_exist @@ -0,0 +1,7 @@ +--uuid:7902e9bd-21a8-4632-8760-d79a67eb89a1 +Content-Id: +Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml" +Content-Transfer-Encoding: binary + +http://www.w3.org/2005/08/addressing/faultuuid:1e213982-4a8d-4722-bb24-a2c5f48dd5c7urn:uuid:93d3b273-c123-4277-b288-24a77b1e90dchttp://www.w3.org/2005/08/addressing/anonymousS:ReceiverLa demande de logement n'existe pas dans le système. +--uuid:7902e9bd-21a8-4632-8760-d79a67eb89a1-- \ No newline at end of file diff --git a/tests/data/sne/response_ok b/tests/data/sne/response_ok new file mode 100644 index 00000000..f1856afc --- /dev/null +++ b/tests/data/sne/response_ok @@ -0,0 +1,13 @@ +--uuid:db03b44e-f563-4ce9-b06d-545faf9b26c0 +Content-Id: +Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml" +Content-Transfer-Encoding: binary + +http://ws.metier.nuu.application.i2/DemandeLogementPortType/getDemandeLogementResponseuuid:1764a9fe-6a19-4833-b104-e253e7c9d6bfurn:uuid:ad0713e6-cfb4-43f5-a69a-12db65925ee3http://www.w3.org/2005/08/addressing/anonymousDEMG1318-202305311447-000001.xml +--uuid:db03b44e-f563-4ce9-b06d-545faf9b26c0 +Content-Id: +Content-Type: text/xml +Content-Transfer-Encoding: binary + +RET2023-05-31T14:47:28.201+02:00GUIPJ0690221008931G31632021-02-11falsefalsetest02/0315000falsetruetruefalse2023-05-28T00:24:11.102+02:00truetruetruetrue695009231Quartile 3694008932Quartile 3691009231Quartile 3697609231Quartile 3693009231Quartile 36974011247Quartile 2693309231Quartile 36948010836Quartile 2TESTDDO-totot-titiTESTDDO-totot-titiTOTO-PEL TOTOPOC TITI26923XXXXXXXXXX1960-02-03falsefalsefalse125db255bataillon test69150false69100false2000false125db255bataillon test69150azztestenfant-azz1994-03-11azztestenfant-azz2015-06-26azztestparent-azz1965-03-28autresenfantazzgigi-toto fifi2019-02-12autresenfantazzmimi-azz1994-02-03azzMonsieurazzMonsieurMister-toto Pelafcrtest Thdz25905XXXXXXXXXX1959-02-05false69009false1500Autre bailleur950566570truefalse6910021965-03-28falsefalsetruefalsefalsetestazzfalse2015-06-26falsefalsetruefalsefalseazz69150 DECINES CHARPIEUfalse +--uuid:db03b44e-f563-4ce9-b06d-545faf9b26c0-- \ No newline at end of file diff --git a/tests/data/sne/xmlmime.xsd b/tests/data/sne/xmlmime.xsd new file mode 100644 index 00000000..766a07bd --- /dev/null +++ b/tests/data/sne/xmlmime.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_sne.py b/tests/test_sne.py new file mode 100644 index 00000000..d6d5455b --- /dev/null +++ b/tests/test_sne.py @@ -0,0 +1,97 @@ +import os + +import pytest +import responses +from django.contrib.contenttypes.models import ContentType +from django.core.files import File + +from passerelle.apps.sne.models import SNE +from passerelle.base.models import AccessRight, ApiUser + + +@pytest.fixture() +def connector(db): + with open('%s/tests/data/sne/cert.pem' % os.getcwd()) as f: + api = ApiUser.objects.create(username='all', keytype='', key='') + connector = SNE.objects.create( + wsdl_url='https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/services/DemandeLogementImplService/?wsdl', + slug='test', + client_certificate=File(f, 'cert.pem'), + certificate_name='CERG1318-202209062200.XXX', + ) + obj_type = ContentType.objects.get_for_model(connector) + AccessRight.objects.create( + codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=connector.pk + ) + return connector + + +def setup_(rsps, settings): + settings.CONNECTORS_SETTINGS = { + "sne/test": { + 'requests_substitutions': [ + { + 'url': 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/', + 'search': 'http://sne2-ws.j2ee.eco.edcs.fr:80', + 'replace': 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid', + } + ] + } + } + with open('%s/tests/data/sne/DemandeLogementImplService.wsdl' % os.getcwd(), 'rb') as f: + rsps.get( + 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/services/DemandeLogementImplService/?wsdl', + status=200, + body=f.read(), + ) + with open('%s/tests/data/sne/DemandeLogementImplService1.wsdl' % os.getcwd(), 'rb') as f: + rsps.get( + 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/services/DemandeLogementImplService?wsdl=1', + status=200, + body=f.read(), + ) + with open('%s/tests/data/sne/DemandeLogementImplService.xsd' % os.getcwd(), 'rb') as f: + rsps.get( + 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/services/DemandeLogementImplService?xsd=1', + status=200, + body=f.read(), + ) + with open('%s/tests/data/sne/xmlmime.xsd' % os.getcwd(), 'rb') as f: + rsps.get('http://www.w3.org/2005/05/xmlmime', status=200, body=f.read()) + + +def test_get_demande_logement(app, connector, settings): + with responses.RequestsMock() as rsps: + setup_(rsps, settings) + with open('%s/tests/data/sne/response_ok' % os.getcwd(), 'rb') as f: + rsps.post( + 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/services/DemandeLogementImplService', + status=200, + body=f.read(), + content_type='multipart/related; start=""; type="application/xop+xml";' + ' boundary="uuid:db03b44e-f563-4ce9-b06d-545faf9b26c0"; start-info="application/soap+xml"', + ) + resp = app.get('/sne/test/get-demande-logement?demand_id=0690221008931G3163') + json_resp = resp.json + assert json_resp['err'] == 0 + assert json_resp['data']['interfaceNuu']['demande']['demandeLogement']['anru'] == 'false' + + +def test_get_demande_logement_does_not_exist(app, connector, settings): + with responses.RequestsMock() as rsps: + setup_(rsps, settings) + with open('%s/tests/data/sne/response_does_not_exist' % os.getcwd(), 'rb') as f: + rsps.post( + 'https://sne-ws-2.site-ecole.din.developpement-durable.gouv.invalid/services/DemandeLogementImplService', + status=200, + body=f.read(), + content_type='multipart/related; start=""; type="application/xop+xml";' + ' boundary="uuid:7902e9bd-21a8-4632-8760-d79a67eb89a1"; start-info="application/soap+xml"', + ) + resp = app.get('/sne/test/get-demande-logement?demand_id=0690221008931G3164') + json_resp = resp.json + assert json_resp['err'] == 1 + assert ( + json_resp['data']['soap_fault']['message'] + == "La demande de logement n'existe pas dans le système." + )