0001-lingo-start-unauthenticated-user-support-in-API-3687.patch
combo/apps/lingo/urls.py | ||
---|---|---|
74 | 74 |
ItemDownloadView.as_view(), name='download-item-pdf'), |
75 | 75 |
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$', |
76 | 76 |
ItemView.as_view(), name='view-item'), |
77 |
url(r'^lingo/item/(?P<item_id>\d+)/pay$', |
|
77 |
url(r'^lingo/item/(?P<item_id>\d+)/(?P<signature>\w+)/pay$',
|
|
78 | 78 |
BasketItemPayView.as_view(), name='basket-item-pay-view'), |
79 | 79 |
url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(), |
80 | 80 |
name='lingo-self-invoice'), |
combo/apps/lingo/views.py | ||
---|---|---|
39 | 39 |
import eopayment |
40 | 40 | |
41 | 41 |
from combo.data.models import Page |
42 |
from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError |
|
42 |
from combo.utils import (check_request_signature, aes_hex_decrypt, aes_hex_encrypt, |
|
43 |
DecryptionError, valid_signature) |
|
43 | 44 |
from combo.profile.utils import get_user_from_name_id |
44 | 45 | |
45 | 46 |
from .models import (Regie, BasketItem, Transaction, TransactionOperation, |
... | ... | |
150 | 151 |
elif request.GET.get('email'): |
151 | 152 |
user = User.objects.get(email=request.GET.get('email')) |
152 | 153 |
else: |
153 |
raise Exception('no user specified')
|
|
154 |
user = None
|
|
154 | 155 |
except User.DoesNotExist: |
155 | 156 |
raise Exception('unknown user') |
156 | 157 | |
... | ... | |
192 | 193 |
'Bad format for capture date, it should be yyyy-mm-dd.') |
193 | 194 | |
194 | 195 |
item.save() |
195 |
item.regie.compute_extra_fees(user=item.user) |
|
196 | ||
197 |
payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id}) |
|
196 |
if user: |
|
197 |
item.regie.compute_extra_fees(user=item.user) |
|
198 | ||
199 |
payment_url = reverse( |
|
200 |
'basket-item-pay-view', |
|
201 |
kwargs={ |
|
202 |
'item_id': item.id, |
|
203 |
'signature': aes_hex_encrypt(settings.SECRET_KEY, str(item.id)) |
|
204 |
}) |
|
198 | 205 |
return JsonResponse({'result': 'success', 'id': str(item.id), |
199 | 206 |
'payment_url': request.build_absolute_uri(payment_url)}) |
200 | 207 | |
... | ... | |
321 | 328 | |
322 | 329 |
class PayMixin(object): |
323 | 330 |
@atomic |
324 |
def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''): |
|
331 |
def handle_payment( |
|
332 |
self, request, regie, items, remote_items, next_url='/', email='', firstname='', |
|
333 |
lastname=''): |
|
325 | 334 |
if remote_items: |
326 | 335 |
total_amount = sum([x.amount for x in remote_items]) |
327 | 336 |
else: |
... | ... | |
344 | 353 |
lastname = user.last_name |
345 | 354 |
else: |
346 | 355 |
transaction.user = None |
347 |
firstname = '' |
|
348 |
lastname = '' |
|
349 | 356 | |
350 | 357 |
transaction.save() |
351 | 358 |
transaction.regie = regie |
... | ... | |
436 | 443 |
class BasketItemPayView(PayMixin, View): |
437 | 444 |
def get(self, request, *args, **kwargs): |
438 | 445 |
next_url = request.GET.get('next_url') or '/' |
439 |
if not (request.user and request.user.is_authenticated): |
|
440 |
return HttpResponseForbidden(_('No item payment allowed for anonymous users.')) |
|
446 |
email = request.GET.get('email', '') |
|
447 |
firstname = request.GET.get('firstname', '') |
|
448 |
lastname = request.GET.get('lastname', '') |
|
449 | ||
450 |
item_id, signature = kwargs.get('item_id'), kwargs.get('signature') |
|
451 |
if not valid_signature(settings.SECRET_KEY, item_id, signature): |
|
452 |
return HttpResponseForbidden(_('Invalid payment request.')) |
|
441 | 453 | |
442 | 454 |
item = BasketItem.objects.get(pk=kwargs['item_id']) |
443 | 455 |
regie = item.regie |
444 | 456 |
if regie.extra_fees_ws_url: |
445 | 457 |
return HttpResponseForbidden(_('No item payment allowed as extra fees set.')) |
446 | 458 | |
447 |
if item.user != request.user: |
|
459 |
if item.user and item.user != request.user:
|
|
448 | 460 |
return HttpResponseForbidden(_('Wrong item: payment not allowed.')) |
449 | 461 | |
450 |
return self.handle_payment(request, regie, [item], [], next_url) |
|
462 |
return self.handle_payment( |
|
463 |
request, regie, [item], [], next_url, email, firstname, lastname |
|
464 |
) |
|
451 | 465 | |
452 | 466 | |
453 | 467 |
class PaymentException(Exception): |
combo/utils/__init__.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
# import specific symbols for compatibility |
18 | 18 |
from .cache import cache_during_request |
19 |
from .crypto import aes_hex_decrypt, aes_hex_encrypt, DecryptionError |
|
19 |
from .crypto import aes_hex_decrypt, aes_hex_encrypt, DecryptionError, valid_signature
|
|
20 | 20 |
from .misc import ellipsize, flatten_context |
21 | 21 |
from .requests_wrapper import requests, NothingInCacheException |
22 | 22 |
from .signature import check_query, check_request_signature, sign_url |
combo/utils/crypto.py | ||
---|---|---|
56 | 56 |
aes_key = PBKDF2(key, iv) |
57 | 57 |
aes = AES.new(aes_key, AES.MODE_CFB, iv) |
58 | 58 |
return force_text(aes.decrypt(crypted), 'utf-8') |
59 | ||
60 | ||
61 |
def valid_signature(key, data, signature): |
|
62 |
try: |
|
63 |
decrypted = aes_hex_decrypt(key, signature) |
|
64 |
except DecryptionError: |
|
65 |
return False |
|
66 |
return decrypted == data |
tests/test_lingo_payment.py | ||
---|---|---|
22 | 22 |
from combo.apps.lingo.models import ( |
23 | 23 |
Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell, |
24 | 24 |
PaymentBackend) |
25 |
from combo.utils import sign_url |
|
25 |
from combo.utils import aes_hex_decrypt, sign_url
|
|
26 | 26 | |
27 | 27 |
from .test_manager import login |
28 | 28 | |
... | ... | |
297 | 297 |
assert resp.status_code == 200 |
298 | 298 |
response = json.loads(resp.text) |
299 | 299 |
assert response['result'] == 'success' |
300 |
assert response['payment_url'].endswith('/lingo/item/%s/pay' % item.id) |
|
300 |
payment_url = urlparse.urlparse(response['payment_url']) |
|
301 |
assert payment_url.path.startswith('/lingo/item/%s/' % item.id) |
|
302 |
assert payment_url.path.endswith('/pay') |
|
301 | 303 |
assert BasketItem.objects.filter(amount=Decimal('22.23')).exists() |
302 | 304 |
assert BasketItem.objects.filter(amount=Decimal('22.23'))[0].regie_id == other_regie.id |
303 | 305 | |
... | ... | |
374 | 376 |
assert 'Can not add a basket item to a remote regie.' in resp.text |
375 | 377 | |
376 | 378 | |
379 |
def test_unauthenticated_user(app, regie): |
|
380 |
url = reverse('api-add-basket-item') |
|
381 |
data = {'amount': 10, 'display_name': 'test item'} |
|
382 |
url = sign_url(url, settings.LINGO_API_SIGN_KEY) |
|
383 |
resp = app.post_json(url, params=data) |
|
384 |
assert resp.status_code == 200 |
|
385 |
payment_url = resp.json['payment_url'] |
|
386 | ||
387 |
item = BasketItem.objects.first() |
|
388 |
assert item.user is None |
|
389 |
assert item.amount == Decimal('10.00') |
|
390 |
path = urlparse.urlparse(payment_url).path |
|
391 |
start = '/lingo/item/%s/' % item.id |
|
392 |
end = '/pay' |
|
393 |
assert path.startswith(start) |
|
394 |
assert path.endswith(end) |
|
395 |
signature = path.replace(start, '').replace(end, '') |
|
396 |
assert aes_hex_decrypt(settings.SECRET_KEY, signature) == str(item.id) |
|
397 | ||
398 |
resp = app.get( |
|
399 |
payment_url, |
|
400 |
params={ |
|
401 |
'next_url': 'http://example.net/form/id/', |
|
402 |
'email': 'foo@localhost', |
|
403 |
'firstname': 'foo', |
|
404 |
'lastname': 'bar' |
|
405 |
} |
|
406 |
) |
|
407 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
|
408 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
|
409 |
assert qs['amount'] == ['10.00'] |
|
410 |
assert qs['email'] == ['foo@localhost'] |
|
411 | ||
412 |
# bad signature |
|
413 |
payment_url = '/lingo/item/%s/xxxxx/pay' % item.id |
|
414 |
resp = app.get(payment_url, status=403) |
|
415 |
assert 'Invalid payment request.' in resp.text |
|
416 | ||
377 | 417 |
def test_cant_pay_if_different_capture_date(app, basket_page, regie, user): |
378 | 418 |
capture1 = (timezone.now() + timedelta(days=1)).date() |
379 | 419 |
capture2 = (timezone.now() + timedelta(days=2)).date() |
... | ... | |
417 | 457 |
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists() |
418 | 458 |
payment_url = resp.json['payment_url'] |
419 | 459 |
resp = app.get(payment_url, status=403) |
420 |
assert 'No item payment allowed for anonymous users.' in resp.text
|
|
460 |
assert 'Wrong item: payment not allowed.' in resp.text
|
|
421 | 461 | |
422 | 462 |
login(app, username='john.doe', password='john.doe') |
423 | 463 |
resp = app.get(payment_url, status=403) |
... | ... | |
440 | 480 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
441 | 481 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
442 | 482 |
assert qs['amount'] == ['12.00'] |
443 | ||
444 | 483 |
# simulate successful payment response from dummy backend |
445 | 484 |
data = {'transaction_id': qs['transaction_id'][0], 'ok': True, |
446 | 485 |
'amount': qs['amount'][0], 'signed': True} |
447 |
- |