Projet

Général

Profil

0001-add-a-generic-soap-connector-60836.patch

Benjamin Dauvergne, 20 janvier 2022 08:50

Télécharger (11,9 ko)

Voir les différences:

Subject: [PATCH] add a generic soap connector (#60836)

 passerelle/apps/soap/__init__.py              |   0
 .../apps/soap/migrations/0001_initial.py      |  83 ++++++++++
 passerelle/apps/soap/migrations/__init__.py   |   0
 passerelle/apps/soap/models.py                | 156 ++++++++++++++++++
 .../templates/soap/soapconnector_detail.html  |   2 +
 passerelle/settings.py                        |   1 +
 6 files changed, 242 insertions(+)
 create mode 100644 passerelle/apps/soap/__init__.py
 create mode 100644 passerelle/apps/soap/migrations/0001_initial.py
 create mode 100644 passerelle/apps/soap/migrations/__init__.py
 create mode 100644 passerelle/apps/soap/models.py
 create mode 100644 passerelle/apps/soap/templates/soap/soapconnector_detail.html
passerelle/apps/soap/migrations/0001_initial.py
1
# Generated by Django 2.2.24 on 2022-01-19 16:37
2

  
3
from django.db import migrations, models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    initial = True
9

  
10
    dependencies = [
11
        ('base', '0029_auto_20210202_1627'),
12
    ]
13

  
14
    operations = [
15
        migrations.CreateModel(
16
            name='SOAPConnector',
17
            fields=[
18
                (
19
                    'id',
20
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
21
                ),
22
                ('title', models.CharField(max_length=50, verbose_name='Title')),
23
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
24
                ('description', models.TextField(verbose_name='Description')),
25
                (
26
                    'basic_auth_username',
27
                    models.CharField(
28
                        blank=True, max_length=128, verbose_name='Basic authentication username'
29
                    ),
30
                ),
31
                (
32
                    'basic_auth_password',
33
                    models.CharField(
34
                        blank=True, max_length=128, verbose_name='Basic authentication password'
35
                    ),
36
                ),
37
                (
38
                    'client_certificate',
39
                    models.FileField(
40
                        blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
41
                    ),
42
                ),
43
                (
44
                    'trusted_certificate_authorities',
45
                    models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'),
46
                ),
47
                (
48
                    'verify_cert',
49
                    models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'),
50
                ),
51
                (
52
                    'http_proxy',
53
                    models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'),
54
                ),
55
                (
56
                    'wsdl_url',
57
                    models.URLField(
58
                        help_text='URL of the WSDL file', max_length=400, verbose_name='WSDL URL'
59
                    ),
60
                ),
61
                (
62
                    'zeep_strict',
63
                    models.BooleanField(default=True, verbose_name='Be strict with returned XML'),
64
                ),
65
                (
66
                    'zeep_xsd_ignore_sequence_order',
67
                    models.BooleanField(default=False, verbose_name='Ignore sequence order'),
68
                ),
69
                (
70
                    'users',
71
                    models.ManyToManyField(
72
                        blank=True,
73
                        related_name='_soapconnector_users_+',
74
                        related_query_name='+',
75
                        to='base.ApiUser',
76
                    ),
77
                ),
78
            ],
79
            options={
80
                'verbose_name': 'SOAP connector',
81
            },
82
        ),
83
    ]
passerelle/apps/soap/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import collections
18

  
19
import zeep
20
import zeep.helpers
21
import zeep.xsd
22
from django.db import models
23
from django.utils.functional import cached_property
24
from django.utils.translation import ugettext_lazy as _
25

  
26
from passerelle.base.models import BaseResource, HTTPResource
27
from passerelle.utils.api import endpoint
28
from passerelle.utils.json import unflatten
29

  
30

  
31
class SOAPConnector(BaseResource, HTTPResource):
32
    wsdl_url = models.URLField(
33
        max_length=400, verbose_name=_('WSDL URL'), help_text=_('URL of the WSDL file')
34
    )
35
    zeep_strict = models.BooleanField(default=False, verbose_name=_('Be strict with returned XML'))
36
    zeep_xsd_ignore_sequence_order = models.BooleanField(
37
        default=True, verbose_name=_('Ignore sequence order')
38
    )
39
    category = _('Business Process Connectors')
40

  
41
    class Meta:
42
        verbose_name = _('SOAP connector')
43

  
44
    @classmethod
45
    def get_manager_form_class(cls, **kwargs):
46
        form_class = super().get_manager_form_class(**kwargs)
47
        fields = list(form_class.base_fields.items())
48
        form_class.base_fields = collections.OrderedDict(fields[:2] + fields[-3:] + fields[2:-3])
49
        return form_class
50

  
51
    @cached_property
52
    def client(self):
53
        return self.soap_client(
54
            wsdl_url=self.wsdl_url,
55
            settings=zeep.Settings(
56
                strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order
57
            ),
58
        )
59

  
60
    @endpoint(
61
        methods=['get', 'post'],
62
        perm='can_access',
63
        name='method',
64
        pattern=r'^(?P<method_name>\w+)/$',
65
        example_pattern='method_name/',
66
        description=_('Call a SOAP method'),
67
    )
68
    def method(self, request, method_name, post_data=None, **kwargs):
69
        def jsonify(data):
70
            if isinstance(data, (dict, collections.OrderedDict)):
71
                # ignore _raw_elements, zeep put there nodes not maching the
72
                # XSD when strict parsing is disabled.
73
                return {
74
                    jsonify(k): jsonify(v)
75
                    for k, v in data.items()
76
                    if (self.zeep_strict or k != '_raw_elements')
77
                }
78
            elif isinstance(data, (list, tuple, collections.deque)):
79
                return [jsonify(item) for item in data]
80
            else:
81
                return data
82

  
83
        payload = {k: request.getlist(k) for k in request.GET if k != 'raise'}
84
        payload.update(unflatten(post_data or {}))
85
        soap_response = getattr(self.client.service, method_name)(**payload)
86
        serialized = zeep.helpers.serialize_object(soap_response)
87
        json_response = jsonify(serialized)
88
        return {'err': 0, 'data': json_response}
89

  
90
    def get_endpoints_infos(self):
91
        endpoints = super().get_endpoints_infos()
92
        for name, input_schema, output_schema in self.operations_and_schemas:
93
            kwargs = dict(
94
                name='method',
95
                pattern=f'{name}/',
96
                example_pattern=f'{name}/',
97
                description=f'Method {name}',
98
                json_schema_response={
99
                    'type': 'object',
100
                    'properties': collections.OrderedDict(
101
                        [
102
                            ('err', {'type': 'integer'}),
103
                            ('data', output_schema),
104
                        ]
105
                    ),
106
                },
107
            )
108
            if input_schema:
109
                kwargs['post_json_schema'] = input_schema
110
            endpoints.append(endpoint(**kwargs))
111
            endpoints[-1].object = self
112
            endpoints[-1].func = self.method
113
            if input_schema:
114
                endpoints[-1].http_method = 'post'
115
            else:
116
                endpoints[-1].http_method = 'get'
117
        return endpoints
118

  
119
    @property
120
    def operations_and_schemas(self):
121
        operations = self.client.service._binding._operations
122
        for name in operations:
123
            operation = operations[name]
124
            input_type = operation.input.body.type
125
            output_type = operation.output.body.type
126
            input_schema = self.type2schema(input_type, root=True)
127
            output_schema = self.type2schema(output_type, root=True)
128
            yield name, input_schema, output_schema
129

  
130
    def type2schema(self, xsd_type, root=False):
131
        # simplify schema: when a type contains a unique element, it will try
132
        # to match any dict or list with it on input and will flatten the
133
        # schema on output.
134
        if isinstance(xsd_type, zeep.xsd.ComplexType) and len(xsd_type.elements) == 1 and not root:
135
            if xsd_type.elements[0][1].max_occurs != 1:
136
                return {'type': 'array', 'items': self.type2schema(xsd_type.elements[0][1].type)}
137
            return self.type2schema(xsd_type.elements[0][1].type)
138
        if isinstance(xsd_type, zeep.xsd.ComplexType):
139
            properties = collections.OrderedDict()
140
            schema = {
141
                'type': 'object',
142
                'properties': properties,
143
            }
144
            for key, element in xsd_type.elements:
145
                if element.min_occurs > 0:
146
                    schema.setdefault('required', []).append(key)
147
                element_schema = self.type2schema(element.type)
148
                if element.max_occurs == 'unbounded' or element.max_occurs > 1:
149
                    element_schema = {'type': 'array', 'items': element_schema}
150
                properties[key] = element_schema
151
            if not properties:
152
                return None
153
            return schema
154
        if isinstance(xsd_type, zeep.xsd.BuiltinType):
155
            return {'type': 'string'}
156
        return f'!!! UNKNOWN TYPE {xsd_type} !!!'
passerelle/apps/soap/templates/soap/soapconnector_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
passerelle/settings.py
161 161
    'passerelle.apps.photon',
162 162
    'passerelle.apps.plone_restapi',
163 163
    'passerelle.apps.sector',
164
    'passerelle.apps.soap',
164 165
    'passerelle.apps.solis',
165 166
    'passerelle.apps.twilio',
166 167
    'passerelle.apps.vivaticket',
167
-