0002-sms-authorize-numbers-using-masks-39650.patch
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 |
- |