From e9dc5baf79e50f42a64a69832f67a6945b07819d Mon Sep 17 00:00:00 2001 From: alangsto <46360176+alangsto@users.noreply.github.com> Date: Thu, 7 Jan 2021 09:05:33 -0500 Subject: [PATCH] added two endpoints for IDV decryption on stage (#25939) simplified public keys made migration fixes for quality added pylint fixes fixed for pylint added endpoint to retrieve user's receipt_ids added tests for 404 with decryption error fixed for quality fixed for quality updates for feedback removed unnecessary method fixed quality issue updated tests --- lms/djangoapps/verify_student/models.py | 82 ++++++- .../verify_student/tests/test_views.py | 214 ++++++++++++++++-- lms/djangoapps/verify_student/urls.py | 12 + lms/djangoapps/verify_student/views.py | 69 +++++- 4 files changed, 350 insertions(+), 27 deletions(-) diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 2d1f8160e3a..430ee98236f 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -37,14 +37,20 @@ from opaque_keys.edx.django.models import CourseKeyField from lms.djangoapps.verify_student.ssencrypt import ( encrypt_and_encode, + decode_and_decrypt, generate_signed_message, random_aes_key, - rsa_encrypt + rsa_encrypt, + rsa_decrypt ) from openedx.core.djangoapps.signals.signals import LEARNER_NOW_VERIFIED from openedx.core.storage import get_storage -from .utils import auto_verify_for_testing_enabled, earliest_allowed_verification_date, submit_request_to_ss +from .utils import ( + auto_verify_for_testing_enabled, + earliest_allowed_verification_date, + submit_request_to_ss +) log = logging.getLogger(__name__) @@ -691,6 +697,14 @@ class SoftwareSecurePhotoVerification(PhotoVerification): except cls.DoesNotExist: return None + def _save_image_to_storage(self, path, img_data): + """ + Given a path and data, save to S3 + Separated out for ease of mocking in testing + """ + buff = ContentFile(img_data) + self._storage.save(path, buff) + @status_before_must_be("created") def upload_face_image(self, img_data): """ @@ -716,9 +730,10 @@ class SoftwareSecurePhotoVerification(PhotoVerification): else: aes_key = aes_key_str.decode("hex") + encrypted_data = encrypt_and_encode(img_data, aes_key) + path = self._get_path("face") - buff = ContentFile(encrypt_and_encode(img_data, aes_key)) - self._storage.save(path, buff) + self._save_image_to_storage(path, encrypted_data) @status_before_must_be("created") def upload_photo_id_image(self, img_data): @@ -748,8 +763,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification): # Save this to the storage backend path = self._get_path("photo_id") - buff = ContentFile(encrypt_and_encode(img_data, aes_key)) - self._storage.save(path, buff) + encrypted_data = encrypt_and_encode(img_data, aes_key) + self._save_image_to_storage(path, encrypted_data) # Update our record fields if six.PY3: @@ -759,6 +774,61 @@ class SoftwareSecurePhotoVerification(PhotoVerification): self.save() + def _get_image_from_storage(self, path): + """ + Given a path, read data from storage and return + Separated for ease of mocking in testing + """ + with self._storage.open(path, mode='rb') as img_file: + byte_img_data = img_file.read() + return byte_img_data + + @status_before_must_be("must_retry", "submitted", "approved", "denied") + def download_face_image(self): + """ + Download the associated face image from storage + """ + if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None): + return None + path = self._get_path("face") + byte_img_data = self._get_image_from_storage(path) + + aes_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"] + + try: + if six.PY3: + aes_key = codecs.decode(aes_key_str, "hex") + else: + aes_key = aes_key_str.decode("hex") + + img_bytes = decode_and_decrypt(byte_img_data, aes_key) + return img_bytes + except: # pylint: disable=bare-except + return None + + @status_before_must_be("must_retry", "submitted", "approved", "denied") + def download_photo_id_image(self): + """ + Download the associated id image from storage + """ + if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None): + return None + + path = self._get_path("photo_id") + byte_img_data = self._get_image_from_storage(path) + + try: + # decode rsa encrypted aes key from base64 + rsa_encrypted_aes_key = base64.urlsafe_b64decode(self.photo_id_key) + + # decrypt aes key using rsa private key + rsa_private_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["RSA_PRIVATE_KEY"] + decrypted_aes_key = rsa_decrypt(rsa_encrypted_aes_key, rsa_private_key_str) + img_bytes = decode_and_decrypt(byte_img_data, decrypted_aes_key) + return img_bytes + except: # pylint: disable=bare-except + return None + @status_before_must_be("must_retry", "ready", "submitted") def submit(self, copy_id_photo_from=None): """ diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index ce763db5439..2b7e9df8b2f 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -6,6 +6,8 @@ Tests of verify_student views. from datetime import timedelta from uuid import uuid4 +import base64 +import codecs import ddt import httpretty import mock @@ -34,6 +36,7 @@ from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService from lms.djangoapps.verify_student.views import PayAndVerifyView, checkout_with_ecommerce_service, render_to_response +from lms.djangoapps.verify_student.ssencrypt import encrypt_and_encode, rsa_encrypt from openedx.core.djangoapps.embargo.test_utils import restrict_course from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme from openedx.core.djangoapps.user_api.accounts.api import get_account_settings @@ -55,6 +58,43 @@ render_mock = Mock(side_effect=mock_render_to_response) PAYMENT_DATA_KEYS = {'payment_processor_name', 'payment_page_url', 'payment_form_data'} +RSA_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1hLVjP0oV0Uy/+jQ+Upz +c+eYc4Pyflb/WpfgYATggkoQdnsdplmvPtQr85+utgqKPxOh+PvYGW8QNUzjLIu4 +5/GlmvBa82i1jRMgEAxGI95bz7j9DtH+7mnj+06zR5xHwT49jK0zMs5MjMaz5WRq +BUNkz7dxWzDrYJZQx230sPp6upy1Y5H5O8SnJVdghsh8sNciS4Bo4ZONQ3giBwxz +h5svjspz1MIsOoShjbAdfG+4VX7sVwYlw2rnQeRsMH5/xpnNeqtScyOMoz0N9UDG +dtRMNGa2MihAg7zh7/zckbUrtf+o5wQtlCJL1Kdj4EjshqYvCxzWnSM+MaYAjb3M +EQIDAQAB +-----END PUBLIC KEY-----""" +RSA_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1hLVjP0oV0Uy/+jQ+Upzc+eYc4Pyflb/WpfgYATggkoQdnsd +plmvPtQr85+utgqKPxOh+PvYGW8QNUzjLIu45/GlmvBa82i1jRMgEAxGI95bz7j9 +DtH+7mnj+06zR5xHwT49jK0zMs5MjMaz5WRqBUNkz7dxWzDrYJZQx230sPp6upy1 +Y5H5O8SnJVdghsh8sNciS4Bo4ZONQ3giBwxzh5svjspz1MIsOoShjbAdfG+4VX7s +VwYlw2rnQeRsMH5/xpnNeqtScyOMoz0N9UDGdtRMNGa2MihAg7zh7/zckbUrtf+o +5wQtlCJL1Kdj4EjshqYvCxzWnSM+MaYAjb3MEQIDAQABAoIBAQCviuA87fdfoOoS +OerrEacc20QDLaby/QoGUtZ2RmmHzY40af7FQ3PWFIw6Ca5trrTwxnuivXnWWWG0 +I2mCRM0Kvfgr1n7ubOW7WnyHTFlT3mnxK2Ov/HmNLZ36nO2cgkXA6/Xy3rBGMC9L +nUE1kSLzT/Fh965ntfS9zmVNNBhb6no0rVkGx5nK3vTI6kUmaa0m+E7KL/HweO4c +JodhN8CX4gpxSrkuwJ7IHEPYspqc0jInMYKLmD3d2g3BiOctjzFmaj3lV5AUlujW +z7/LVe5WAEaaxjwaMvwqrJLv9ogxWU3etJf22+Yy7r5gbPtqpqJrCZ5+WpGnUHws +3mMGP2QBAoGBAOc3pzLFgGUREVPSFQlJ06QFtfKYqg9fFHJCgWu/2B2aVZc2aO/t +Zhuoz+AgOdzsw+CWv7K0FH9sUkffk2VKPzwwwufLK3avD9gI0bhmBAYvdhS6A3nO +YM3W+lvmaJtFL00K6kdd+CzgRnBS9cZ70WbcbtqjdXI6+mV1WdGUTLhBAoGBAO0E +xhD4z+GjubSgfHYEZPgRJPqyUIfDH+5UmFGpr6zlvNN/depaGxsbhW8t/V6xkxsG +MCgic7GLMihEiUMx1+/snVs5bBUx7OT9API0d+vStHCFlTTe6aTdmiduFD4PbDsq +6E4DElVRqZhpIYusdDh7Z3fO2hm5ad4FfMlx65/RAoGAPYEfV7ETs06z9kEG2X6q +7pGaUZrsecRH8xDfzmKswUshg2S0y0WyCJ+CFFNeMPdGL4LKIWYnobGVvYqqcaIr +af5qijAQMrTkmQnXh56TaXXMijzk2czdEUQjOrjykIL5zxudMDi94GoUMqLOv+qF +zD/MuRoMDsPDgaOSrd4t/kECgYEAzwBNT8NOIz3P0Z4cNSJPYIvwpPaY+IkE2SyO +vzuYj0Mx7/Ew9ZTueXVGyzv6PfqOhJqZ8mNscZIlIyAAVWwxsHwRTfvPlo882xzP +97i1R4OFTYSNNFi+69sSZ/9utGjZ2K73pjJuj487tD2VK5xZAH9edTd2KeNSP7LB +MlpJNBECgYAmIswPdldm+G8SJd5j9O2fcDVTURjKAoSXCv2j4gEZzzfudpLWNHYu +l8N6+LEIVTMAytPk+/bImHvGHKZkCz5rEMSuYJWOmqKI92rUtI6fz5DUb3XSbrwT +3W+sdGFUK3GH1NAX71VxbAlFVLUetcMwai1+wXmGkRw6A7YezVFnhw== +-----END RSA PRIVATE KEY-----""" + def _mock_payment_processors(): """ @@ -1259,14 +1299,7 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase): "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006", "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7", "FACE_IMAGE_AES_KEY": "f82400259e3b4f88821cd89838758292", - "RSA_PUBLIC_KEY": ( - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkgtz3fQdiXshy/RfOHkoHlhx/" - "SSPZ+nNyE9JZXtwhlzsXjnu+e9GOuJzgh4kUqo73ePIG5FxVU+mnacvufq2cu1SOx" - "lRYGyBK7qDf9Ym67I5gmmcNhbzdKcluAuDCPmQ4ecKpICQQldrDQ9HWDxwjbbcqpVB" - "PYWkE1KrtypGThmcehLmabf6SPq1CTAGlXsHgUtbWCwV6mqR8yScV0nRLln0djLDm9d" - "L8tIVFFVpAfBaYYh2Cm5EExQZjxyfjWd8P5H+8/l0pmK2jP7Hc0wuXJemIZbsdm+DSD" - "FhCGY3AILGkMwr068dGRxfBtBy/U9U5W+nStvkDdMrSgQezS5+V test@example.com" - ), + "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY, "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1", "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881", "S3_BUCKET": "test.example.com", @@ -1784,14 +1817,7 @@ class TestReverifyView(TestVerificationBase): "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006", "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7", "FACE_IMAGE_AES_KEY": "f82400259e3b4f88821cd89838758292", - "RSA_PUBLIC_KEY": ( - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkgtz3fQdiXshy/RfOHkoHlhx/" - "SSPZ+nNyE9JZXtwhlzsXjnu+e9GOuJzgh4kUqo73ePIG5FxVU+mnacvufq2cu1SOx" - "lRYGyBK7qDf9Ym67I5gmmcNhbzdKcluAuDCPmQ4ecKpICQQldrDQ9HWDxwjbbcqpVB" - "PYWkE1KrtypGThmcehLmabf6SPq1CTAGlXsHgUtbWCwV6mqR8yScV0nRLln0djLDm9d" - "L8tIVFFVpAfBaYYh2Cm5EExQZjxyfjWd8P5H+8/l0pmK2jP7Hc0wuXJemIZbsdm+DSD" - "FhCGY3AILGkMwr068dGRxfBtBy/U9U5W+nStvkDdMrSgQezS5+V test@example.com" - ), + "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY, "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1", "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881", "CERT_VERIFICATION_PATH": False, @@ -1803,9 +1829,9 @@ class TestReverifyView(TestVerificationBase): }, }, ) -class TestPhotoURLView(ModuleStoreTestCase, TestVerificationBase): +class TestPhotoURLView(TestVerificationBase): """ - Tests for the results_callback view. + Tests for the photo url view. """ def setUp(self): @@ -1853,3 +1879,155 @@ class TestPhotoURLView(ModuleStoreTestCase, TestVerificationBase): url = reverse('verification_photo_urls', kwargs={'receipt_id': six.text_type(self.receipt_id)}) response = self.client.get(url) self.assertEqual(response.status_code, 403) + + +@override_settings( + VERIFY_STUDENT={ + "SOFTWARE_SECURE": { + "API_URL": "https://verify.example.com/submit/", + "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006", + "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7", + "FACE_IMAGE_AES_KEY": b'32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae', + "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY, + "RSA_PRIVATE_KEY": RSA_PRIVATE_KEY, + "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1", + "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881", + "S3_BUCKET": "test-idv", + "CERT_VERIFICATION_PATH": False, + }, + "DAYS_GOOD_FOR": 10, + "STORAGE_CLASS": 'storages.backends.s3boto.S3BotoStorage', + "STORAGE_KWARGS": { + 'bucket': 'test-idv', + }, + } +) +@ddt.ddt +class TestDecodeImageViews(MockS3BotoMixin, TestVerificationBase): + """ + Test for both face and photo id image decoding views + """ + + IMAGE_DATA = "abcd,1234" + + def setUp(self): + super().setUp() + self.user = AdminFactory() + login_success = self.client.login(username=self.user.username, password='test') + self.assertTrue(login_success) + + def _mock_submit_images(self): + """ + Mocks submitting images for IDV and saving to S3 + """ + # create an attempt with a submitted status, and create a photo_id_key to use + # for decryption + attempt = SoftwareSecurePhotoVerification( + status="submitted", + user=self.user + ) + rsa_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["RSA_PUBLIC_KEY"] + rsa_encrypted_aes_key = rsa_encrypt( + codecs.decode( + settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"], + "hex" + ), + rsa_key_str + ) + attempt.photo_id_key = codecs.encode(rsa_encrypted_aes_key, 'base64').decode('utf-8') + + attempt.save() + + def _decode_image(self, receipt_id, img_type): + """ + Test function used to call decoding endpoint + Arg: + receipt_id(str): receipt ID for endpoint url + img_type(str): 'face' or 'photo_id', used to determine which endpoint to use + """ + url_name = 'verification_decrypt_face_image' + if img_type == 'photo_id': + url_name = 'verification_decrypt_photo_id_image' + url = reverse(url_name, kwargs={'receipt_id': six.text_type(receipt_id)}) + + response = self.client.get(url) + + return response + + @ddt.data("face", "photo_id") + @patch.object(SoftwareSecurePhotoVerification, '_get_image_from_storage') + def test_download_image_response(self, img_type, _mock_get_storage): + _mock_get_storage.return_value = encrypt_and_encode( + b'\xd7m\xf8', + codecs.decode(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"], "hex") + ) + # upload 'images' + self._mock_submit_images() + attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) + receipt_id = attempt.receipt_id + + #mock downloading and decrypting images + response = self._decode_image(receipt_id, img_type) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, base64.b64decode(self.IMAGE_DATA.split(",")[1])) + + @ddt.data("face", "photo_id") + def test_403_for_non_staff(self, img_type): + self.user = UserFactory() + login_success = self.client.login(username=self.user.username, password='test') + self.assertTrue(login_success) + + self._mock_submit_images() + attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) + receipt_id = attempt.receipt_id + + # mock downloading and decrypting images + response = self._decode_image(receipt_id, img_type) + self.assertEqual(response.status_code, 403) + + @override_settings( + VERIFY_STUDENT={ + "SOFTWARE_SECURE": { + "API_URL": "https://verify.example.com/submit/", + "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006", + "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7", + "FACE_IMAGE_AES_KEY": b'32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae', + "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY, + "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1", + "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881", + "S3_BUCKET": "test-idv", + "CERT_VERIFICATION_PATH": False, + }, + "DAYS_GOOD_FOR": 10, + } + ) + @ddt.data("face", "photo_id") + def test_403_for_non_staging(self, img_type): + self._mock_submit_images() + attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) + receipt_id = attempt.receipt_id + + # mock downloading and decrypting images + response = self._decode_image(receipt_id, img_type) + self.assertEqual(response.status_code, 403) + + @ddt.data("face", "photo_id") + def test_404_if_invalid_receipt_id(self, img_type): + response = self._decode_image('00000000-0000-0000-0000-000000000000', img_type) + self.assertEqual(response.status_code, 404) + + @ddt.data("face", "photo_id") + @patch.object(SoftwareSecurePhotoVerification, '_get_image_from_storage') + def test_404_for_decryption_error(self, img_type, _mock_get_storage): + _mock_get_storage.return_value = None + # create verification with no img data + attempt = SoftwareSecurePhotoVerification( + status="submitted", + user=self.user + ) + attempt.save() + receipt_id = attempt.receipt_id + + # mock downloading and decrypting images + response = self._decode_image(receipt_id, img_type) + self.assertEqual(response.status_code, 404) diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index 39f103611be..db258bbeb45 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -106,6 +106,18 @@ urlpatterns = [ views.PhotoUrlsView.as_view(), name="verification_photo_urls" ), + + url( + r'^decrypt-idv-images/face/{receipt_id}$'.format(receipt_id=IDV_RECEIPT_ID_PATTERN), + views.DecryptFaceImageView.as_view(), + name="verification_decrypt_face_image" + ), + + url( + r'^decrypt-idv-images/photo-id/{receipt_id}$'.format(receipt_id=IDV_RECEIPT_ID_PATTERN), + views.DecryptPhotoIDImageView.as_view(), + name="verification_decrypt_photo_id_image" + ), ] # Fake response page for incourse reverification ( software secure ) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index e1b88610a49..3138e4205f9 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -12,7 +12,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.staticfiles.storage import staticfiles_storage from django.db import transaction -from django.http import Http404, HttpResponse, HttpResponseBadRequest +from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import redirect from django.urls import reverse from django.utils.decorators import method_decorator @@ -24,7 +24,6 @@ from django.views.decorators.http import require_POST from django.views.generic.base import View from edx_rest_api_client.exceptions import SlumberBaseException from ipware.ip import get_ip -from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from rest_framework.response import Response from rest_framework.views import APIView @@ -1225,7 +1224,7 @@ class PhotoUrlsView(APIView): def get(self, request, receipt_id): """ Endpoint for retrieving photo urls for IDV - GET /verify_student/photo_urls/{receipt_id} + GET /verify_student/photo-urls/{receipt_id} Returns: 200 OK @@ -1247,3 +1246,67 @@ class PhotoUrlsView(APIView): log.warning(u"Could not find verification with receipt ID %s.", receipt_id) raise Http404 + + +class DecryptFaceImageView(APIView): + """ + Endpoint to retrieve decrypted IDV face image data. Can only be used on stage. + """ + + @method_decorator(require_global_staff) + def get(self, request, receipt_id): + """ + Endpoint used for decrypting images on stage based on a given receipt ID + GET /verify_student/decrypt-idv-images/face/{receipt_id} + + Returns: + 200 OK + { + img + } + """ + # if this endpoint is not being accessed on stage, raise a 403. Only stage will have an RSA_PRIVATE_KEY + if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None): + log.warning(u"Cannot access image decryption outside of staging environment") + return HttpResponseForbidden() + + verification = SoftwareSecurePhotoVerification.get_verification_from_receipt(receipt_id) + if verification: + user_photo = verification.download_face_image() + if user_photo: + return HttpResponse(user_photo, content_type="image/png") + + log.warning(u"Could not decrypt face image for receipt ID %s.", receipt_id) + raise Http404 + + +class DecryptPhotoIDImageView(APIView): + """ + Endpoint to retrieve decrypted IDV photo ID image data. Can only be used on stage. + """ + + @method_decorator(require_global_staff) + def get(self, request, receipt_id): + """ + Endpoint used for decrypting images on stage based on a given receipt ID + GET /verify_student/decrypt-idv-images/photo-id/{receipt_id} + + Returns: + 200 OK + { + img + } + """ + # if this endpoint is not being accessed on stage, raise a 403. Only stage will have an RSA_PRIVATE_KEY + if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None): + log.warning(u"Cannot access image decryption outside of staging environment") + return HttpResponseForbidden() + + verification = SoftwareSecurePhotoVerification.get_verification_from_receipt(receipt_id) + if verification: + id_photo = verification.download_photo_id_image() + if id_photo: + return HttpResponse(id_photo, content_type="image/png") + + log.warning(u"Could not decrypt photo ID image for receipt ID %s.", receipt_id) + raise Http404 -- GitLab