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 |
# |
... | ... | |
157 | 158 |
elif number.startswith(self.default_trunk_prefix): |
158 | 159 |
number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):] |
159 | 160 |
else: |
160 | 161 |
raise APIError('phone number %r is unsupported (no international prefix, ' |
161 | 162 |
'no local trunk prefix)' % number) |
162 | 163 |
numbers.append(number) |
163 | 164 |
return numbers |
164 | 165 | |
166 |
def authorize_numbers(self, destinations): |
|
167 |
unknown_numbers = set() |
|
168 |
premium_numbers = set() |
|
169 |
regexes = [] |
|
170 |
if SMSResource.ALL not in self.authorized: |
|
171 |
if SMSResource.FR_METRO in self.authorized: |
|
172 |
regexes.append(r'0033[67]\d{8}') # France |
|
173 |
if SMSResource.FR_DOMTOM in self.authorized: |
|
174 |
regexes.append(r'00262262\d{6}') # Réunion, Mayotte, Terres australe/antarctiques |
|
175 |
regexes.append(r'508508\d{6}') # Saint-Pierre-et-Miquelon |
|
176 |
regexes.append(r'590590\d{6}') # Guadeloupe, Saint-Barthélemy, Saint-Martin |
|
177 |
regexes.append(r'594594\d{6}') # Guyane |
|
178 |
regexes.append(r'596596\d{6}') # Martinique |
|
179 |
regexes.append(r'00687[67]\d{8}') # Nouvelle-Calédonie |
|
180 |
if SMSResource.BE_ in self.authorized: |
|
181 |
regexes.append(r'00324[5-9]\d{7}') # Belgian |
|
182 | ||
183 |
unknown_numbers = set(destinations) |
|
184 |
for value in regexes: |
|
185 |
regex = re.compile(value) |
|
186 |
unknown_numbers = set(dest for dest in unknown_numbers if not regex.match(dest)) |
|
187 | ||
188 |
for number in list(unknown_numbers): |
|
189 |
logging.warning('phone number does not match any authorization mask: %s', number) |
|
190 | ||
191 |
if SMSResource.NO_ == self.premium_rate: |
|
192 |
regex = re.compile(r'0033[8]\d{8}') |
|
193 |
premium_numbers = set(dest for dest in destinations if regex.match(dest)) |
|
194 |
for number in list(premium_numbers): |
|
195 |
logging.warning('primium rate phone number: %s', number) |
|
196 | ||
197 |
unauthorized_numbers = list(unknown_numbers.union(premium_numbers)) |
|
198 |
if unauthorized_numbers != []: |
|
199 |
raise APIError('phone numbers not authorized: %s' % ', '.join(unauthorized_numbers)) |
|
200 |
return True |
|
201 | ||
165 | 202 |
@endpoint(perm='can_send_messages', methods=['post'], |
166 | 203 |
description=_('Send a SMS message'), |
167 | 204 |
post={'request_body': {'schema': {'application/json': SEND_SCHEMA}}}) |
168 | 205 |
def send(self, request, post_data): |
169 | 206 |
post_data['message'] = post_data['message'][:self.max_message_length] |
170 | 207 |
post_data['to'] = self.clean_numbers(post_data['to']) |
208 |
self.authorize_numbers(post_data['to']) |
|
171 | 209 |
logging.info('sending SMS to %r from %r', post_data['to'], post_data['from']) |
172 | 210 |
stop = not bool('nostop' in request.GET) |
173 | 211 |
self.add_job('send_job', |
174 | 212 |
text=post_data['message'], sender=post_data['from'], destinations=post_data['to'], |
175 | 213 |
stop=stop) |
176 | 214 |
return {'err': 0} |
177 | 215 | |
178 | 216 |
def send_job(self, *args, **kwargs): |
tests/test_sms.py | ||
---|---|---|
9 | 9 |
# This program is distributed in the hope that it will be useful, |
10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | 12 |
# GNU Affero General Public License for more details. |
13 | 13 |
# |
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 |
import isodate |
17 |
import logging |
|
17 | 18 |
import mock |
18 | 19 |
import pytest |
19 | 20 |
from requests import RequestException |
20 | 21 | |
21 | 22 |
from django.contrib.contenttypes.models import ContentType |
22 | 23 |
from django.utils.translation import ugettext as _ |
23 | 24 | |
24 | 25 |
from passerelle.apps.choosit.models import ChoositSMSGateway |
25 | 26 |
from passerelle.apps.ovh.models import OVHSMSGateway |
26 |
from passerelle.base.models import ApiUser, AccessRight, Job |
|
27 |
from passerelle.base.models import ApiUser, AccessRight, Job, ResourceLog
|
|
27 | 28 |
from passerelle.sms.models import SMSResource, SMSLog |
28 | 29 |
from passerelle.utils.jsonresponse import APIError |
29 | 30 | |
30 | 31 |
from test_manager import login, admin_user |
31 | 32 | |
32 | 33 |
import utils |
33 | 34 | |
34 | 35 |
pytestmark = pytest.mark.django_db |
... | ... | |
46 | 47 |
connector.save() |
47 | 48 |
assert connector.clean_numbers(['+ 33 12']) == ['003312'] |
48 | 49 |
assert connector.clean_numbers(['0 0 33 12']) == ['003312'] |
49 | 50 |
assert connector.clean_numbers(['1 12']) == ['003212'] |
50 | 51 |
with pytest.raises(APIError, match='phone number %r is unsupported' % '0123'): |
51 | 52 |
connector.clean_numbers(['0123']) |
52 | 53 | |
53 | 54 | |
55 |
def test_authorize_numbers(): |
|
56 |
connector = OVHSMSGateway() |
|
57 | ||
58 |
# premium-rate |
|
59 |
assert connector.premium_rate == SMSResource.NO_ |
|
60 |
number = '0033' + '8' + '12345678' |
|
61 |
with pytest.raises(APIError, match='phone numbers not authorized: %s' % number): |
|
62 |
assert connector.authorize_numbers([number]) |
|
63 |
connector.premium_rate = SMSResource.YES |
|
64 |
connector.save() |
|
65 | ||
66 |
# All country |
|
67 |
assert connector.authorized == [SMSResource.ALL] |
|
68 |
number = '0033' + '1' + '12345678' |
|
69 |
assert connector.authorize_numbers([number]) |
|
70 |
connector.authorized = [SMSResource.FR_METRO] |
|
71 |
connector.save() |
|
72 |
with pytest.raises(APIError, match='phone numbers not authorized: %s' % number): |
|
73 |
connector.authorize_numbers([number]) |
|
74 | ||
75 |
# France |
|
76 |
number = '0033' + '6' + '12345678' |
|
77 |
assert connector.authorize_numbers([number]) |
|
78 |
connector.authorized = [SMSResource.FR_DOMTOM] |
|
79 |
connector.save() |
|
80 |
with pytest.raises(APIError, match='phone numbers not authorized: %s' % number): |
|
81 |
connector.authorize_numbers([number]) |
|
82 | ||
83 |
# Dom-Tom |
|
84 |
number = '596596' + '123456' |
|
85 |
assert connector.authorize_numbers([number]) |
|
86 |
connector.authorized = [SMSResource.BE_] |
|
87 |
connector.save() |
|
88 |
with pytest.raises(APIError, match='phone numbers not authorized: %s' % number): |
|
89 |
connector.authorize_numbers([number]) |
|
90 | ||
91 |
# Belgian |
|
92 |
number = '0032' + '45' + '1234567' |
|
93 |
assert connector.authorize_numbers([number]) |
|
94 |
connector.authorized = [SMSResource.FR_METRO] |
|
95 |
connector.save() |
|
96 |
with pytest.raises(APIError, match='phone numbers not authorized: %s' % number): |
|
97 |
connector.authorize_numbers([number]) |
|
98 | ||
99 | ||
54 | 100 |
@pytest.fixture(params=klasses) |
55 | 101 |
def connector(request, db): |
56 | 102 |
klass = request.param |
57 | 103 |
kwargs = getattr(klass, 'TEST_DEFAULTS', {}).get('create_kwargs', {}) |
58 | 104 |
kwargs.update({ |
59 | 105 |
'title': klass.__name__, |
60 | 106 |
'slug': klass.__name__.lower(), |
61 | 107 |
'description': klass.__name__, |
... | ... | |
251 | 297 |
'There were errors processing your form.' |
252 | 298 |
resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.' |
253 | 299 |
resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM] |
254 | 300 |
resp = resp.form.submit() |
255 | 301 |
resp = resp.follow() |
256 | 302 |
assert _('France mainland (+33 [67])') in [ |
257 | 303 |
x.text for x in resp.html.find_all('p') |
258 | 304 |
if x.text.startswith(_('Authorized Countries'))][0] |
305 | ||
306 |
path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) |
|
307 |
payload = { |
|
308 |
'message': 'plop', |
|
309 |
'from': '+33699999999', |
|
310 |
'to': ['+33688888888'], |
|
311 |
} |
|
312 |
app.post_json(path, params=payload) |
|
313 |
with mock.patch.object(type(connector), 'send_msg') as send_function: |
|
314 |
send_function.return_value = {} |
|
315 |
connector.jobs() |
|
316 |
assert SMSLog.objects.count() == 1 |
|
317 | ||
318 |
payload['to'][0] = '+33188888888' |
|
319 |
SMSLog.objects.all().delete() |
|
320 |
app.post_json(path, params=payload) |
|
321 |
with mock.patch.object(type(connector), 'send_msg') as send_function: |
|
322 |
send_function.return_value = {} |
|
323 |
connector.jobs() |
|
324 |
assert not SMSLog.objects.count() |
|
325 |
assert ResourceLog.objects.filter(levelno=logging.WARNING).count() == 1 |
|
326 |
assert ResourceLog.objects.filter(levelno=30)[0].extra['exception'] == \ |
|
327 |
'phone numbers not authorized: 0033188888888' |
|
259 |
- |