0001-sms-add-description-to-send-endpoint-45829.patch
passerelle/sms/models.py | ||
---|---|---|
20 | 20 |
from django.utils import six |
21 | 21 |
from django.utils.translation import ugettext_lazy as _ |
22 | 22 | |
23 | 23 |
from passerelle.base.models import BaseResource |
24 | 24 |
from passerelle.compat import json_loads |
25 | 25 |
from passerelle.utils.api import endpoint |
26 | 26 |
from passerelle.utils.jsonresponse import APIError |
27 | 27 | |
28 |
SEND_SCHEMA = { |
|
29 |
'$schema': 'http://json-schema.org/draft-04/schema#', |
|
30 |
"type": "object", |
|
31 |
'required': ['message', 'from', 'to'], |
|
32 |
'properties': { |
|
33 |
'message': { |
|
34 |
'description': 'String message', |
|
35 |
'type': 'string', |
|
36 |
}, |
|
37 |
'from': { |
|
38 |
'description': 'Sender number', |
|
39 |
'type': 'string', |
|
40 |
}, |
|
41 |
'to': { |
|
42 |
'description': 'Destination numbers', |
|
43 |
"type": "array", |
|
44 |
"items": { |
|
45 |
'type': 'string', |
|
46 |
'pattern': r'^\+?\d+$' |
|
47 |
}, |
|
48 |
}, |
|
49 |
} |
|
50 |
} |
|
51 | ||
28 | 52 | |
29 | 53 |
class SMSResource(BaseResource): |
30 | 54 |
category = _('SMS Providers') |
31 | 55 |
documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/' |
32 | 56 | |
33 | 57 |
_can_send_messages_description = _('Sending messages is limited to the following API users:') |
34 | 58 | |
35 | 59 |
default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3, |
... | ... | |
56 | 80 |
elif number.startswith(self.default_trunk_prefix): |
57 | 81 |
number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):] |
58 | 82 |
else: |
59 | 83 |
raise APIError('phone number %r is unsupported (no international prefix, ' |
60 | 84 |
'no local trunk prefix)' % number) |
61 | 85 |
numbers.append(number) |
62 | 86 |
return numbers |
63 | 87 | |
64 |
@endpoint(perm='can_send_messages', methods=['post']) |
|
65 |
def send(self, request, *args, **kwargs): |
|
66 |
try: |
|
67 |
data = json_loads(request.body) |
|
68 |
assert isinstance(data, dict), 'JSON payload is not a dict' |
|
69 |
assert 'message' in data, 'missing "message" in JSON payload' |
|
70 |
assert 'from' in data, 'missing "from" in JSON payload' |
|
71 |
assert 'to' in data, 'missing "to" in JSON payload' |
|
72 |
assert isinstance(data['message'], six.text_type), 'message is not a string' |
|
73 |
assert isinstance(data['from'], six.text_type), 'from is not a string' |
|
74 |
assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \ |
|
75 |
'to is not a list of strings' |
|
76 |
except (ValueError, AssertionError) as e: |
|
77 |
raise APIError('Payload error: %s' % e) |
|
78 |
data['message'] = data['message'][:self.max_message_length] |
|
79 |
data['to'] = self.clean_numbers(data['to']) |
|
80 |
logging.info('sending SMS to %r from %r', data['to'], data['from']) |
|
88 |
@endpoint(perm='can_send_messages', methods=['post'], |
|
89 |
description=_('Send a SMS message'), |
|
90 |
post={'request_body': {'schema': {'application/json': SEND_SCHEMA}}}) |
|
91 |
def send(self, request, post_data): |
|
92 |
post_data['message'] = post_data['message'][:self.max_message_length] |
|
93 |
post_data['to'] = self.clean_numbers(post_data['to']) |
|
94 |
logging.info('sending SMS to %r from %r', post_data['to'], post_data['from']) |
|
81 | 95 |
stop = not bool('nostop' in request.GET) |
82 | 96 |
self.add_job('send_job', |
83 |
text=data['message'], sender=data['from'], destinations=data['to'],
|
|
97 |
text=post_data['message'], sender=post_data['from'], destinations=post_data['to'],
|
|
84 | 98 |
stop=stop) |
85 | 99 |
return {'err': 0} |
86 | 100 | |
87 | 101 |
def send_job(self, *args, **kwargs): |
88 | 102 |
self.send_msg(**kwargs) |
89 | 103 |
SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug) |
90 | 104 | |
91 | 105 |
class Meta: |
passerelle/sms/templates/passerelle/manage/messages_service_view.html | ||
---|---|---|
1 | 1 |
{% extends "passerelle/manage/service_view.html" %} |
2 | 2 |
{% load i18n passerelle %} |
3 | 3 | |
4 | 4 |
{% block endpoints %} |
5 |
<ul> |
|
6 |
<li>{% trans 'Sending a message:' %} <a href="send" >{{ site_base_uri }}{{ object.get_absolute_url }}send</a> (POST)</li> |
|
7 |
</ul> |
|
5 |
{{ block.super }} |
|
8 | 6 |
{% endblock %} |
tests/test_api_access.py | ||
---|---|---|
37 | 37 |
description='access for all', |
38 | 38 |
keytype='', key='') |
39 | 39 |
obj_type = ContentType.objects.get_for_model(OxydSMSGateway) |
40 | 40 |
AccessRight.objects.create(codename='can_send_messages', |
41 | 41 |
apiuser=api, |
42 | 42 |
resource_type=obj_type, |
43 | 43 |
resource_pk=oxyd.pk, |
44 | 44 |
) |
45 |
resp = app.post_json(endpoint_url, params={}) |
|
45 |
resp = app.post_json(endpoint_url, params={}, status=400)
|
|
46 | 46 |
# for empty payload the connector returns an APIError with |
47 |
# {"err_desc": "missing \"message\" in JSON payload"}
|
|
47 |
# {"err_desc": "'message' is a required property"}
|
|
48 | 48 |
assert resp.json['err'] == 1 |
49 |
assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload' |
|
49 |
assert resp.json['err_desc'] == "'message' is a required property" |
|
50 | ||
50 | 51 | |
51 | 52 |
def test_access_with_signature(app, oxyd): |
52 | 53 |
api = ApiUser.objects.create(username='eservices', |
53 | 54 |
fullname='Eservices User', |
54 | 55 |
description='eservices', |
55 | 56 |
keytype='SIGN', |
56 | 57 |
key='12345') |
57 | 58 |
obj_type = ContentType.objects.get_for_model(OxydSMSGateway) |
... | ... | |
60 | 61 |
apiuser=api, |
61 | 62 |
resource_type=obj_type, |
62 | 63 |
resource_pk=oxyd.pk, |
63 | 64 |
) |
64 | 65 |
endpoint_url = reverse('generic-endpoint', |
65 | 66 |
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'}) |
66 | 67 |
url = signature.sign_url(endpoint_url + '?orig=eservices', '12345') |
67 | 68 |
# for empty payload the connector returns an APIError with |
68 |
# {"err_desc": "missing \"message\" in JSON payload"}
|
|
69 |
resp = app.post_json(url, params={}) |
|
69 |
# {"err_desc": "'message' is a required property"}
|
|
70 |
resp = app.post_json(url, params={}, status=400)
|
|
70 | 71 |
assert resp.json['err'] == 1 |
71 |
assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload' |
|
72 |
assert resp.json['err_desc'] == "'message' is a required property" |
|
73 | ||
72 | 74 |
# bad key |
73 | 75 |
url = signature.sign_url(endpoint_url + '?orig=eservices', 'notmykey') |
74 | 76 |
resp = app.post_json(url, params={}, status=403) |
75 | 77 |
assert resp.json['err'] == 1 |
76 | 78 |
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied' |
77 | 79 |
# add garbage after signature |
78 | 80 |
url = signature.sign_url(endpoint_url + '?orig=eservices', '12345') |
79 | 81 |
url = '%s&foo=bar' % url |
80 | 82 |
resp = app.post_json(url, params={}, status=403) |
81 | 83 |
assert resp.json['err'] == 1 |
82 | 84 |
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied' |
83 | 85 | |
84 | 86 |
# trusted user (from settings.KNOWN_SERVICES) |
85 | 87 |
url = signature.sign_url(endpoint_url + '?orig=wcs1', 'abcde') |
86 |
resp = app.post_json(url, params={}) |
|
88 |
resp = app.post_json(url, params={}, status=400)
|
|
87 | 89 |
assert resp.json['err'] == 1 |
88 |
assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload' |
|
90 |
assert resp.json['err_desc'] == "'message' is a required property" |
|
91 | ||
89 | 92 |
# bad key |
90 | 93 |
url = signature.sign_url(endpoint_url + '?orig=wcs1', 'notmykey') |
91 | 94 |
resp = app.post_json(url, params={}, status=403) |
92 | 95 |
assert resp.json['err'] == 1 |
93 | 96 |
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied' |
94 | 97 | |
95 | 98 | |
96 | 99 |
def test_access_http_auth(app, oxyd): |
... | ... | |
106 | 109 |
AccessRight.objects.create(codename='can_send_messages', |
107 | 110 |
apiuser=api, |
108 | 111 |
resource_type=obj_type, |
109 | 112 |
resource_pk=oxyd.pk, |
110 | 113 |
) |
111 | 114 |
app.authorization = ('Basic', (username, password)) |
112 | 115 |
endpoint_url = reverse('generic-endpoint', |
113 | 116 |
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'}) |
114 |
resp = app.post_json(endpoint_url, params={}) |
|
117 |
resp = app.post_json(endpoint_url, params={}, status=400)
|
|
115 | 118 |
assert resp.json['err'] == 1 |
116 |
assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload' |
|
119 |
assert resp.json['err_desc'] == "'message' is a required property" |
|
120 | ||
117 | 121 | |
118 | 122 |
def test_access_apikey(app, oxyd): |
119 | 123 |
password = 'apiuser_12345' |
120 | 124 |
api = ApiUser.objects.create(username='apiuser', |
121 | 125 |
fullname='Api User', |
122 | 126 |
description='api', |
123 | 127 |
keytype='API', |
124 | 128 |
key=password) |
... | ... | |
127 | 131 |
AccessRight.objects.create(codename='can_send_messages', |
128 | 132 |
apiuser=api, |
129 | 133 |
resource_type=obj_type, |
130 | 134 |
resource_pk=oxyd.pk, |
131 | 135 |
) |
132 | 136 |
params = {'message': 'test'} |
133 | 137 |
endpoint_url = reverse('generic-endpoint', |
134 | 138 |
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'}) |
135 |
resp = app.post_json(endpoint_url + '?apikey=' + password , params=params) |
|
139 |
resp = app.post_json(endpoint_url + '?apikey=' + password , params=params, status=400)
|
|
136 | 140 |
resp.json['err'] == 1 |
137 |
assert resp.json['err_desc'] == 'Payload error: missing "from" in JSON payload'
|
|
141 |
assert resp.json['err_desc'] == "'from' is a required property"
|
|
138 | 142 |
resp = app.post_json(endpoint_url + '?apikey=' + password[:3] , params=params, status=403) |
139 | 143 |
resp.json['err'] == 1 |
140 | 144 |
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied' |
141 | 145 | |
146 | ||
142 | 147 |
def test_access_apiuser_with_no_key(app, oxyd): |
143 | 148 |
api = ApiUser.objects.create(username='apiuser', |
144 | 149 |
fullname='Api User', |
145 | 150 |
description='api') |
146 | 151 |
obj_type = ContentType.objects.get_for_model(OxydSMSGateway) |
147 | 152 | |
148 | 153 |
AccessRight.objects.create(codename='can_send_messages', |
149 | 154 |
apiuser=api, |
150 | 155 |
resource_type=obj_type, |
151 | 156 |
resource_pk=oxyd.pk, |
152 | 157 |
) |
153 | 158 |
params = {'message': 'test', 'from': 'test api'} |
154 | 159 |
endpoint_url = reverse('generic-endpoint', |
155 | 160 |
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'}) |
156 |
resp = app.post_json(endpoint_url, params=params) |
|
161 |
resp = app.post_json(endpoint_url, params=params, status=400)
|
|
157 | 162 |
assert resp.json['err'] == 1 |
158 |
assert resp.json['err_desc'] == 'Payload error: missing "to" in JSON payload' |
|
163 |
assert resp.json['err_desc'] == "'to' is a required property" |
|
164 | ||
159 | 165 | |
160 | 166 |
def test_access_apiuser_with_ip_restriction(app, oxyd): |
161 | 167 |
authorized_ip = '176.31.123.109' |
162 | 168 |
api = ApiUser.objects.create(username='apiuser', |
163 | 169 |
fullname='Api User', |
164 | 170 |
description='api', |
165 | 171 |
ipsource=authorized_ip |
166 | 172 |
) |
... | ... | |
176 | 182 |
resp = app.post_json(endpoint_url, params={}, extra_environ={'REMOTE_ADDR': '127.0.0.1'}, |
177 | 183 |
status=403) |
178 | 184 |
assert resp.json['err'] == 1 |
179 | 185 |
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied' |
180 | 186 | |
181 | 187 |
endpoint_url = reverse('generic-endpoint', |
182 | 188 |
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'}) |
183 | 189 |
resp = app.post_json(endpoint_url, params={}, |
184 |
extra_environ={'REMOTE_ADDR': authorized_ip}) |
|
190 |
extra_environ={'REMOTE_ADDR': authorized_ip}, status=400)
|
|
185 | 191 |
assert resp.json['err'] == 1 |
186 |
assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload' |
|
192 |
assert resp.json['err_desc'] == "'message' is a required property" |
tests/test_sms.py | ||
---|---|---|
51 | 51 |
apiuser=api, |
52 | 52 |
resource_type=obj_type, |
53 | 53 |
resource_pk=c.pk) |
54 | 54 |
return c |
55 | 55 | |
56 | 56 | |
57 | 57 |
def test_connectors(app, connector, freezer): |
58 | 58 |
path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) |
59 |
result = app.post_json(path, params={}) |
|
59 |
result = app.post_json(path, params={}, status=400)
|
|
60 | 60 |
assert result.json['err'] == 1 |
61 |
assert result.json['err_desc'].startswith('Payload error: ')
|
|
61 |
assert result.json['err_desc'] == "'message' is a required property"
|
|
62 | 62 | |
63 | 63 |
payload = { |
64 | 64 |
'message': 'hello', |
65 | 65 |
'from': '+33699999999', |
66 | 66 |
'to': ['+33688888888', '+33677777777'], |
67 | 67 |
} |
68 | 68 |
test_vectors = getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', []) |
69 | 69 |
total = len(test_vectors) |
70 |
- |