Projet

Général

Profil

0002-sms-authorize-numbers-using-masks-39650.patch

Nicolas Roche, 07 mai 2020 15:12

Télécharger (9,09 ko)

Voir les différences:

Subject: [PATCH 2/2] sms: authorize numbers using masks (#39650)

 passerelle/sms/models.py | 38 ++++++++++++++++++++++
 tests/test_sms.py        | 68 +++++++++++++++++++++++++++++++++++++++-
 2 files changed, 105 insertions(+), 1 deletion(-)
passerelle/sms/models.py
1
# -*- coding: utf-8 -*-
1 2
# passerelle - uniform access to multiple data sources and services
2 3
# Copyright (C) 2020  Entr'ouvert
3 4
#
4 5
# This program is free software: you can redistribute it and/or modify it
5 6
# under the terms of the GNU Affero General Public License as published
6 7
# by the Free Software Foundation, either version 3 of the License, or
7 8
# (at your option) any later version.
8 9
#
......
128 129
            elif number.startswith(self.default_trunk_prefix):
129 130
                number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):]
130 131
            else:
131 132
                raise APIError('phone number %r is unsupported (no international prefix, '
132 133
                               'no local trunk prefix)' % number)
133 134
            numbers.append(number)
134 135
        return numbers
135 136

  
137
    def authorize_numbers(self, destinations):
138
        unknown_numbers = set()
139
        premium_numbers = set()
140
        regexes = []
141
        if SMSResource.ALL not in self.authorized:
142
            if SMSResource.FR_METRO in self.authorized:
143
                regexes.append(r'0033[67]\d{8}')    # France
144
            if SMSResource.FR_DOMTOM in self.authorized:
145
                regexes.append(r'00262262\d{6}')    # Réunion, Mayotte, Terres australe/antarctiques
146
                regexes.append(r'508508\d{6}')      # Saint-Pierre-et-Miquelon
147
                regexes.append(r'590590\d{6}')      # Guadeloupe, Saint-Barthélemy, Saint-Martin
148
                regexes.append(r'594594\d{6}')      # Guyane
149
                regexes.append(r'596596\d{6}')      # Martinique
150
                regexes.append(r'00687[67]\d{8}')   # Nouvelle-Calédonie
151
            if SMSResource.BE in self.authorized:
152
                regexes.append(r'00324[5-9]\d{7}')  # Belgian
153

  
154
            unknown_numbers = set(destinations)
155
            for value in regexes:
156
                regex = re.compile(value)
157
                unknown_numbers = set(dest for dest in unknown_numbers if not regex.match(dest))
158

  
159
            for number in list(unknown_numbers):
160
                logging.warning('phone number does not match any authorization mask: %s', number)
161

  
162
        if SMSResource.NO == self.premium_rate:
163
            regex = re.compile(r'0033[8]\d{8}')
164
            premium_numbers = set(dest for dest in destinations if regex.match(dest))
165
        for number in list(premium_numbers):
166
            logging.warning('primium rate phone number: %s', number)
167

  
168
        unauthorized_numbers = list(unknown_numbers.union(premium_numbers))
169
        if unauthorized_numbers != []:
170
            raise APIError('phone numbers not authorized: %s' % ', '.join(unauthorized_numbers))
171
        return True
172

  
136 173
    @endpoint(perm='can_send_messages', methods=['post'])
137 174
    def send(self, request, *args, **kwargs):
138 175
        try:
139 176
            data = json_loads(request.body)
140 177
            assert isinstance(data, dict), 'JSON payload is not a dict'
141 178
            assert 'message' in data, 'missing "message" in JSON payload'
142 179
            assert 'from' in data, 'missing "from" in JSON payload'
143 180
            assert 'to' in data, 'missing "to" in JSON payload'
144 181
            assert isinstance(data['message'], six.text_type), 'message is not a string'
145 182
            assert isinstance(data['from'], six.text_type), 'from is not a string'
146 183
            assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \
147 184
                'to is not a list of strings'
148 185
        except (ValueError, AssertionError) as e:
149 186
            raise APIError('Payload error: %s' % e)
150 187
        data['message'] = data['message'][:self.max_message_length]
151 188
        data['to'] = self.clean_numbers(data['to'])
189
        self.authorize_numbers(data['to'])
152 190
        stop = not bool('nostop' in request.GET)
153 191
        logging.info('sending message %r to %r with sending number %r',
154 192
                     data['message'], data['to'], data['from'])
155 193
        # unfortunately it lacks a batch API...
156 194
        result = {'data': self.send_msg(data['message'], data['from'], data['to'], stop=stop)}
157 195
        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug)
158 196
        return result
159 197

  
tests/test_sms.py
2 2
import mock
3 3
import pytest
4 4

  
5 5
from django.contrib.contenttypes.models import ContentType
6 6
from django.utils.translation import ugettext as _
7 7

  
8 8
from passerelle.apps.choosit.models import ChoositSMSGateway
9 9
from passerelle.apps.ovh.models import OVHSMSGateway
10
from passerelle.base.models import ApiUser, AccessRight
10
from passerelle.base.models import ApiUser, AccessRight, ResourceLog
11 11
from passerelle.sms.models import SMSResource, SMSLog
12 12
from passerelle.utils.jsonresponse import APIError
13 13

  
14 14
from test_manager import login, admin_user
15 15

  
16 16
import utils
17 17

  
18 18
pytestmark = pytest.mark.django_db
......
30 30
    connector.save()
31 31
    assert connector.clean_numbers(['+ 33 12']) == ['003312']
32 32
    assert connector.clean_numbers(['0 0 33 12']) == ['003312']
33 33
    assert connector.clean_numbers(['1 12']) == ['003212']
34 34
    with pytest.raises(APIError, match='phone number %r is unsupported' % '0123'):
35 35
        connector.clean_numbers(['0123'])
36 36

  
37 37

  
38
def test_authorize_numbers():
39
    connector = OVHSMSGateway()
40

  
41
    # premium-rate
42
    assert connector.premium_rate == SMSResource.NO
43
    number = '0033' + '8' + '12345678'
44
    with pytest.raises(APIError, match='phone numbers not authorized: %s' % number):
45
        assert connector.authorize_numbers([number])
46
    connector.premium_rate = SMSResource.YES
47
    connector.save()
48

  
49
    # All country
50
    assert connector.authorized == [SMSResource.ALL]
51
    number = '0033' + '1' + '12345678'
52
    assert connector.authorize_numbers([number])
53
    connector.authorized = [SMSResource.FR_METRO]
54
    connector.save()
55
    with pytest.raises(APIError, match='phone numbers not authorized: %s' % number):
56
        connector.authorize_numbers([number])
57

  
58
    # France
59
    number = '0033' + '6' + '12345678'
60
    assert connector.authorize_numbers([number])
61
    connector.authorized = [SMSResource.FR_DOMTOM]
62
    connector.save()
63
    with pytest.raises(APIError, match='phone numbers not authorized: %s' % number):
64
        connector.authorize_numbers([number])
65

  
66
    # Dom-Tom
67
    number = '596596' + '123456'
68
    assert connector.authorize_numbers([number])
69
    connector.authorized = [SMSResource.BE]
70
    connector.save()
71
    with pytest.raises(APIError, match='phone numbers not authorized: %s' % number):
72
        connector.authorize_numbers([number])
73

  
74
    # Belgian
75
    number = '0032' + '45' + '1234567'
76
    assert connector.authorize_numbers([number])
77
    connector.authorized = [SMSResource.FR_METRO]
78
    connector.save()
79
    with pytest.raises(APIError, match='phone numbers not authorized: %s' % number):
80
        connector.authorize_numbers([number])
81

  
82

  
38 83
@pytest.fixture(params=klasses)
39 84
def connector(request, db):
40 85
    klass = request.param
41 86
    kwargs = getattr(klass, 'TEST_DEFAULTS', {}).get('create_kwargs', {})
42 87
    kwargs.update({
43 88
        'title': klass.__name__,
44 89
        'slug': klass.__name__.lower(),
45 90
        'description': klass.__name__,
......
140 185
        'There were errors processing your form.'
141 186
    resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
142 187
    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
143 188
    resp = resp.form.submit()
144 189
    resp = resp.follow()
145 190
    assert _('France mainland (+33 [67])') in [
146 191
        x.text for x in resp.html.find_all('p')
147 192
        if x.text.startswith(_('Authorized Countries'))][0]
193

  
194
    path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
195
    payload = {
196
        'message': 'plop',
197
        'from': '+33699999999',
198
        'to': ['+33688888888'],
199
    }
200
    with mock.patch.object(type(connector), 'send_msg') as send_function:
201
        send_function.return_value = {}
202
        app.post_json(path, params=payload)
203
    assert SMSLog.objects.count() == 1
204

  
205
    payload['to'][0] = '+33188888888'
206
    SMSLog.objects.all().delete()
207
    with mock.patch.object(type(connector), 'send_msg') as send_function:
208
        send_function.return_value = {}
209
        app.post_json(path, params=payload)
210
    assert not SMSLog.objects.count()
211
    assert ResourceLog.objects.filter(levelno=logging.WARNING).count() == 1
212
    assert ResourceLog.objects.filter(levelno=30)[0].extra['exception'] == \
213
        'phone numbers not authorized: 0033188888888'
148
-