diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index 88f45f024d..738728042b 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -82,7 +82,7 @@ $(document).ready(function() {
${_("Why do I have to pay?")}
-

${_("As a not-for-profit, edX uses your contribution to support our mission to provide quality education to everyone around the world. While we have established a minimum fee, we ask that you contribute as much as you can.")}

+

${_("As a not-for-profit, edX uses your contribution to support our mission to provide quality education to everyone around the world, and to improve learning through research. While we have established a minimum fee, we ask that you contribute as much as you can.")}

${_("I'd like to pay more than the minimum. Is my contribution tax deductible?")}
@@ -93,7 +93,7 @@ $(document).ready(function() { % if "honor" in modes:
${_("What if I can't afford it or don't have the necessary equipment?")}
-

${_("If you can't afford the minimum fee or don't meet the requirements, you can audit the course for free. You may also elect to pursue an Honor Code certificate, but you will need to tell us why you would like the fee waived below. Then click the 'Select Certificate' button to complete your registration.")}

+

${_("If you can't afford the minimum fee or don't meet the requirements, you can audit the course or elect to pursue an honor code certificate at no cost. If you would like to pursue the honor code certificate, please check the honor code certificate box, tell us why you can't pursue the verified certificate below, and then click the 'Select Certificate' button to complete your registration.")}

diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index a7f1caaca7..5a1821de54 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -356,6 +356,26 @@ class PhotoVerification(StatusModel): self.status = "denied" self.save() + @status_before_must_be("must_retry", "submitted", "approved", "denied") + def system_error(self, + error_msg, + error_code="", + reviewing_user=None, + reviewing_service=""): + """ + Mark that this attempt could not be completed because of a system error. + Status should be moved to `must_retry`. + """ + if self.status in ["approved", "denied"]: + return # If we were already approved or denied, just leave it. + + self.error_msg = error_msg + self.error_code = error_code + self.reviewing_user = reviewing_user + self.reviewing_service = reviewing_service + self.status = "must_retry" + self.save() + class SoftwareSecurePhotoVerification(PhotoVerification): """ @@ -500,7 +520,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): header_txt = "\n".join( "{}: {}".format(h, v) for h,v in sorted(headers.items()) ) - body_txt = json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False) + body_txt = json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8') return header_txt + "\n\n" + body_txt @@ -509,7 +529,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification): response = requests.post( settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"], headers=headers, - data=json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False) + data=json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8'), + verify=False ) log.debug("Sent request to Software Secure for {}".format(self.receipt_id)) log.debug("Headers:\n{}\n\n".format(headers)) diff --git a/lms/djangoapps/verify_student/ssencrypt.py b/lms/djangoapps/verify_student/ssencrypt.py index 862c5aa021..aefb4292a0 100644 --- a/lms/djangoapps/verify_student/ssencrypt.py +++ b/lms/djangoapps/verify_student/ssencrypt.py @@ -127,9 +127,7 @@ def generate_signed_message(method, headers_dict, body_dict, access_key, secret_ """ Returns a (message, signature) pair. """ - headers_str = "{}\n\n{}".format(method, header_string(headers_dict)) - body_str = body_string(body_dict) - message = headers_str + body_str + message = signing_format_message(method, headers_dict, body_dict) # hmac needs a byte string for it's starting key, can't be unicode. hashed = hmac.new(secret_key.encode('utf-8'), message, sha256) @@ -139,6 +137,18 @@ def generate_signed_message(method, headers_dict, body_dict, access_key, secret_ message += '\n' return message, signature, authorization_header +def signing_format_message(method, headers_dict, body_dict): + """ + Given a dictionary of headers and a dictionary of the JSON for the body, + will return a str that represents the normalized version of this messsage + that will be used to generate a signature. + """ + headers_str = "{}\n\n{}".format(method, header_string(headers_dict)) + body_str = body_string(body_dict) + message = headers_str + body_str + + return message + def header_string(headers_dict): """Given a dictionary of headers, return a canonical string representation.""" header_list = [] @@ -152,17 +162,26 @@ def header_string(headers_dict): return "".join(header_list) # Note that trailing \n's are important -def body_string(body_dict): +def body_string(body_dict, prefix=""): """ - This version actually doesn't support nested lists and dicts. The code for - that was a little gnarly and we don't use that functionality, so there's no - real test for correctness. + Return a canonical string representation of the body of a JSON request or + response. This canonical representation will be used as an input to the + hashing used to generate a signature. """ body_list = [] for key, value in sorted(body_dict.items()): - if value is None: - value = "null" - body_list.append(u"{}:{}\n".format(key, value).encode('utf-8')) + if isinstance(value, (list, tuple)): + for i, arr in enumerate(value): + if isinstance(arr, dict): + body_list.append(body_string(arr, u"{}.{}.".format(key, i))) + else: + body_list.append(u"{}.{}:{}\n".format(key, i, arr).encode('utf-8')) + elif isinstance(value, dict): + body_list.append(body_string(value, key + ":")) + else: + if value is None: + value = "null" + body_list.append(u"{}{}:{}\n".format(prefix, key, value).encode('utf-8')) return "".join(body_list) # Note that trailing \n's are important diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index f315c2136f..e1ec69f724 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -180,21 +180,43 @@ def results_callback(request): settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_SECRET_KEY"] ) - if not sig_valid: - return HttpResponseBadRequest(_("Signature is invalid")) + _, access_key_and_sig = headers["Authorization"].split(" ") + access_key = access_key_and_sig.split(":")[0] + + # This is what we should be doing... + #if not sig_valid: + # return HttpResponseBadRequest("Signature is invalid") + + # This is what we're doing until we can figure out why we disagree on sigs + if access_key != settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_ACCESS_KEY"]: + return HttpResponseBadRequest("Access key invalid") receipt_id = body_dict.get("EdX-ID") result = body_dict.get("Result") reason = body_dict.get("Reason", "") error_code = body_dict.get("MessageType", "") - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=receipt_id) - if result == "PASSED": + try: + attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=receipt_id) + except SoftwareSecurePhotoVerification.DoesNotExist: + log.error("Software Secure posted back for receipt_id {}, but not found".format(receipt_id)) + return HttpResponseBadRequest("edX ID {} not found".format(receipt_id)) + + if result == "PASS": + log.debug("Approving verification for {}".format(receipt_id)) attempt.approve() - elif result == "FAILED": - attempt.deny(reason, error_code=error_code) + elif result == "FAIL": + log.debug("Denying verification for {}".format(receipt_id)) + attempt.deny(json.dumps(reason), error_code=error_code) elif result == "SYSTEM FAIL": + log.debug("System failure for {} -- resetting to must_retry".format(receipt_id)) + attempt.system_error(json.dumps(reason), error_code=error_code) log.error("Software Secure callback attempt for %s failed: %s", receipt_id, reason) + else: + log.error("Software Secure returned unknown result {}".format(result)) + return HttpResponseBadRequest( + "Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL".format(result) + ) return HttpResponse("OK!") diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 0f5de94c76..30e7183317 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -256,4 +256,4 @@ BROKER_URL = "{0}://{1}:{2}@{3}/{4}".format(CELERY_BROKER_TRANSPORT, CELERY_BROKER_VHOST) # Student identity verification settings -VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", "") +VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", VERIFY_STUDENT) diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index a78d723b01..6be1eba6f1 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -372,6 +372,10 @@ .copy { @extend .copy-detail; } + + strong { + color: $m-gray-d2; + } } // ==================== @@ -695,6 +699,10 @@ @extend .copy-detail; } + .example { + color: $m-gray-l2; + } + // help - general list .list-help { margin-top: ($baseline/2); @@ -1406,6 +1414,10 @@ // VIEW: requirements &.step-requirements { + .help-item-technical { + display: none; + } + // progress nav .progress .progress-step { @@ -1636,6 +1648,11 @@ // VIEW: review photos &.step-review { + + .help-item-technical { + display: none; + } + .modal.edit-name .submit input { color: #fff; } @@ -1655,22 +1672,38 @@ border: none; } } - } .nav-wizard { - .help-inline { - width: flex-grid(4,12); - margin-top: 0 + .prompt-verify { + float: left; + width: flex-grid(6,12); + margin: 0 flex-gutter() 0 0; + + .title { + @extend .hd-lv4; + margin-bottom: ($baseline/4); + } + + .copy { + @extend .t-copy-sub1; + @extend .t-weight3; + } + + .list-actions { + margin-top: ($baseline/2); + } + + .action-verify label { + @extend .t-copy-sub1; + } } .wizard-steps { - float: right; - width: flex-grid(8,12); + margin-top: ($baseline/2); .wizard-step { - width: flex-grid(4,8); margin-right: flex-gutter(); display: inline-block; vertical-align: middle; @@ -1681,13 +1714,6 @@ } } - .step-match { - - label { - @extend .t-copy-sub1; - } - } - .step-proceed { } @@ -1733,6 +1759,10 @@ // VIEW: confirmation/receipt &.step-confirmation { + .help-item-technical { + display: none; + } + // progress nav .progress .progress-step { diff --git a/lms/templates/shoppingcart/verified_cert_receipt.html b/lms/templates/shoppingcart/verified_cert_receipt.html index b341ad3edd..3d999570fc 100644 --- a/lms/templates/shoppingcart/verified_cert_receipt.html +++ b/lms/templates/shoppingcart/verified_cert_receipt.html @@ -100,7 +100,7 @@ ${_("Course")} ${_("Status")} - ${_("Options")} + ${_("Options")} @@ -113,9 +113,8 @@ %if course_has_started: - ${_("Starts: {start_date}").format(start_date=course_start_date_text)} + ${_("Go to Course")} %else: - ${_("Go to Course")} %endif diff --git a/lms/templates/verify_student/_verification_support.html b/lms/templates/verify_student/_verification_support.html index e2f598674f..0503c3bf7d 100644 --- a/lms/templates/verify_student/_verification_support.html +++ b/lms/templates/verify_student/_verification_support.html @@ -3,19 +3,26 @@
diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html index b64a3c3dd3..537c26ff84 100644 --- a/lms/templates/verify_student/photo_verification.html +++ b/lms/templates/verify_student/photo_verification.html @@ -155,7 +155,8 @@
  • ${_("Make sure your face is well-lit")}
  • ${_("Be sure your entire face is inside the frame")}
  • ${_("Can we match the photo you took with the one on your ID?")}
  • -
  • ${_("Click the checkmark once you are happy with the photo")}
  • +
  • ${_("Once in position, use the camera button")} () ${_("to capture your picture")}
  • +
  • ${_("Use the checkmark button")} () ${_("once you are happy with the photo")}
  • @@ -244,7 +245,8 @@
  • ${_("Ensure that you can see your photo and read your name")}
  • ${_("Try to keep your fingers at the edge to avoid covering important information")}
  • ${_("Acceptable IDs include drivers licenses, passports, or other goverment-issued IDs that include your name and photo")}
  • -
  • ${_("Click the checkmark once you are happy with the photo")}
  • +
  • ${_("Once in position, use the camera button")} () ${_("to capture your ID")}
  • +
  • ${_("Use the checkmark button")} () ${_("once you are happy with the photo")}
  • @@ -367,13 +369,20 @@