passerelle/passerelle/apps/esirius/models.py

250 lines
9.6 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
import base64
from copy import deepcopy
from time import time
from urllib.parse import urljoin
from Cryptodome.Cipher import DES
from Cryptodome.Util.Padding import pad
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import force_bytes
from django.utils.translation import ugettext_lazy as _
from passerelle.base.models import BaseResource, HTTPResource
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
CREATE_APPOINTMENT_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
"type": "object",
'properties': {
'idSys': {'type': 'string', 'pattern': '^[0-9]*$'},
'codeRDV': {'type': 'string'},
'beginDate': {'type': 'string', 'pattern': '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'},
'beginTime': {'type': 'string', 'pattern': '^[0-9]{2}:[0-9]{2}$'},
'endDate': {'type': 'string', 'pattern': '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'},
'endTime': {'type': 'string', 'pattern': '^[0-9]{2}:[0-9]{2}$'},
'comment': {'type': 'string'},
'isoLanguage': {'description': 'ex: fr', 'type': 'string'},
'needsConfirmation': {'description': 'boolean expected', 'type': 'string'},
'rdvChannel': {'description': 'ex: EAPP0', 'type': 'string'},
'receptionChannel': {'type': 'string'},
'owner': {'type': 'object', 'properties': {'key': {'type': 'string'}, 'value': {'type': 'string'}}},
'user': {
'type': 'object',
'properties': {
'idSys': {'type': 'string', 'pattern': '^[0-9]*$'},
'personalIdentity': {'type': 'string'},
'additionalPersonalIdentity': {"type": "array", "items": {'type': 'string'}},
'lastName': {'type': 'string'},
'civility': {'type': 'string'},
'firstName': {'type': 'string'},
'birthday': {'type': 'string'},
'email': {'type': 'string'},
'fixPhone': {'type': 'string'},
'phone': {'type': 'string'},
'address': {
'type': 'object',
'properties': {
'line1': {'type': 'string'},
'line2': {'type': 'string'},
'zipCode': {'type': 'string'},
'city': {'type': 'string'},
'country': {'type': 'string'},
},
},
},
},
'serviceId': {'type': 'string'},
'siteCode': {'type': 'string'},
"resources": {
'type': 'object',
'properties': {
'id': {'type': 'string', 'pattern': '^[0-9]*$'},
'key': {'type': 'string'},
'type': {'type': 'string'},
'name': {'type': 'string'},
'station': {
'type': 'object',
'properties': {
'id': {'type': 'string', 'pattern': '^[0-9]*$'},
'key': {'type': 'string'},
'name': {'type': 'string'},
},
},
},
},
'motives': {
"type": "array",
"items": {
'type': 'object',
'properties': {
'id': {'type': 'string', 'pattern': '^[0-9]*$'},
'name': {'type': 'string'},
'shortName': {'type': 'string'},
'processingTime': {'type': 'string', 'pattern': '^[0-9]*$'},
'externalModuleAccess': {'type': 'string', 'pattern': '^[0-9]*$'},
'quantity': {'type': 'string', 'pattern': '^[0-9]*$'},
'usePremotiveQuantity': {'description': 'boolean expected', 'type': 'string'},
},
},
},
},
'unflatten': True,
}
UPDATE_APPOINTMENT_SCHEMA = deepcopy(CREATE_APPOINTMENT_SCHEMA)
class DESKeyModel(models.CharField):
def clean(self, value, model_instance):
if len(value) < 8:
raise ValidationError(_('DES key must be 8 bytes long (longer keys are truncated)'))
return super().clean(value, model_instance)
class ESirius(BaseResource, HTTPResource):
secret_id = models.CharField(max_length=128, verbose_name=_('Application identifier'), blank=True)
secret_key = DESKeyModel(max_length=128, verbose_name=_('Secret Key'), blank=True)
base_url = models.CharField(
max_length=256,
blank=False,
verbose_name=_('ePlanning webservices URL'),
help_text=_('example: https://HOST/ePlanning/webservices/api/'),
)
category = _('Business Process Connectors')
class Meta:
verbose_name = _('eSirius')
def request(self, uri, method='GET', params=None, json=None):
url = urljoin(self.base_url, uri)
headers = {'Accept': 'application/json; charset=utf-8'}
if self.secret_key:
des_key = pad(force_bytes(self.secret_key), 8)[:8]
cipher = DES.new(des_key, DES.MODE_ECB)
epoch = int(time() * 1000)
plaintext = '{"caller":"%s","createInfo":%i}' % (self.secret_id, epoch)
msg = cipher.encrypt(pad(force_bytes(plaintext), 8))
headers['token_info_caller'] = base64.b64encode(msg)
response = self.requests.request(method=method, url=url, headers=headers, params=params, json=json)
# handle strange 304 delete response
if method == 'DELETE' and response.status_code == 304:
raise APIError('Appointment not found')
if response.status_code != 200:
try:
json_content = response.json()
except ValueError:
json_content = None
raise APIError(
'error status:%s %r, content:%r'
% (response.status_code, response.reason, response.text[:1024]),
data={'status_code': response.status_code, 'json_content': json_content},
)
return response
def check_status(self):
"""
Raise an exception if something goes wrong.
"""
self.request('sites/', method='GET')
@endpoint(
display_category=_('Appointment'),
description=_('Create appointment'),
name='create-appointment',
perm='can_access',
methods=['post'],
post={'request_body': {'schema': {'application/json': CREATE_APPOINTMENT_SCHEMA}}},
)
def create_appointment(self, request, post_data):
# address dict is required
if not post_data.get('user'):
post_data['user'] = {}
if not post_data['user'].get('address'):
post_data['user']['address'] = {}
response = self.request('appointments/', method='POST', json=post_data)
return {'data': {'id': response.text, 'created': True}}
@endpoint(
display_category=_('Appointment'),
description=_('Update appointment'),
name='update-appointment',
perm='can_access',
methods=['post'],
parameters={
'id': {
'description': _('Appointment id returned by create-appointment endpoint'),
'example_value': '94PEP4',
},
},
post={'request_body': {'schema': {'application/json': UPDATE_APPOINTMENT_SCHEMA}}},
)
def update_appointment(self, request, id, post_data):
# address dict is required
if not post_data.get('user'):
post_data['user'] = {}
if not post_data['user'].get('address'):
post_data['user']['address'] = {}
post_data['codeRDV'] = id
response = self.request('appointments', method='PUT', json=post_data)
return {'data': {'id': id, 'updated': True}}
@endpoint(
display_category=_('Appointment'),
description=_('Get appointment'),
name='get-appointment',
perm='can_access',
methods=['get'],
parameters={
'id': {
'description': _('Appointment id returned by create-appointment endpoint'),
'example_value': '94PEP4',
},
},
)
def get_appointment(self, request, id):
response = self.request('appointments/%s/' % id, method='GET')
return {'data': response.json()}
@endpoint(
display_category=_('Appointment'),
description=_('Delete appointment'),
name='delete-appointment',
perm='can_access',
methods=['delete'],
parameters={
'id': {
'description': _('Appointment id returned by create-appointment endpoint'),
'example_value': '94PEP4',
},
},
)
def delete_appointment(self, request, id):
response = self.request('appointments/%s/' % id, method='DELETE')
return {'data': {'id': id, 'deleted': True}}