Skip to content
Snippets Groups Projects
Commit 4ee9ef61 authored by Diana Huang's avatar Diana Huang
Browse files

Clean up some old pep8/pylint violations

Also, deletes some unused code.
parent 3ca74c01
No related merge requests found
"""
Views for the course_mode module
"""
import decimal
from django.core.urlresolvers import reverse
from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404
HttpResponseBadRequest, Http404
)
from django.shortcuts import redirect
from django.views.generic.base import View
from django.utils.translation import ugettext as _
from django.utils.http import urlencode
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
......@@ -18,10 +21,19 @@ from student.models import CourseEnrollment
from student.views import course_from_id
from verify_student.models import SoftwareSecurePhotoVerification
class ChooseModeView(View):
class ChooseModeView(View):
"""
View used when the user is asked to pick a mode
When a get request is used, shows the selection page.
When a post request is used, assumes that it is a form submission
from the selection page, parses the response, and then sends user
to the next step in the flow
"""
@method_decorator(login_required)
def get(self, request, course_id, error=None):
""" Displays the course mode choice page """
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
return redirect(reverse('dashboard'))
modes = CourseMode.modes_for_course_dict(course_id)
......@@ -34,8 +46,8 @@ class ChooseModeView(View):
"course_id": course_id,
"modes": modes,
"course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default,
"course_num" : course.display_number_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"chosen_price": chosen_price,
"error": error,
}
......@@ -48,6 +60,7 @@ class ChooseModeView(View):
@method_decorator(login_required)
def post(self, request, course_id):
""" Takes the form submission from the page and parses it """
user = request.user
# This is a bit redundant with logic in student.views.change_enrollement,
......@@ -102,6 +115,10 @@ class ChooseModeView(View):
)
def get_requested_mode(self, user_choice):
"""
Given the text of `user_choice`, return the
corresponding course mode slug
"""
choices = {
"Select Audit": "audit",
"Select Certificate": "verified"
......
......@@ -26,12 +26,11 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from model_utils.models import StatusModel
from model_utils import Choices
from verify_student.ssencrypt import (
random_aes_key, decode_and_decrypt, encrypt_and_encode,
random_aes_key, encrypt_and_encode,
generate_signed_message, rsa_encrypt
)
......@@ -57,15 +56,18 @@ def status_before_must_be(*valid_start_statuses):
distracting boilerplate when looking at a Model that needs to go through a
workflow process.
"""
def decorator_func(fn):
@functools.wraps(fn)
def decorator_func(func):
"""
Decorator function that gets returned
"""
@functools.wraps(func)
def with_status_check(obj, *args, **kwargs):
if obj.status not in valid_start_statuses:
exception_msg = (
u"Error calling {} {}: status is '{}', must be one of: {}"
).format(fn, obj, obj.status, valid_start_statuses)
).format(func, obj, obj.status, valid_start_statuses)
raise VerificationException(exception_msg)
return fn(obj, *args, **kwargs)
return func(obj, *args, **kwargs)
return with_status_check
......@@ -367,7 +369,7 @@ class PhotoVerification(StatusModel):
Status should be moved to `must_retry`.
"""
if self.status in ["approved", "denied"]:
return # If we were already approved or denied, just leave it.
return # If we were already approved or denied, just leave it.
self.error_msg = error_msg
self.error_code = error_code
......@@ -408,7 +410,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
# encode that. The result is saved here. Actual expected length is 344.
photo_id_key = models.TextField(max_length=1024)
IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds
IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds
@status_before_must_be("created")
def upload_face_image(self, img_data):
......@@ -444,8 +446,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
self.status = "must_retry"
self.error_msg = response.text
self.save()
except Exception as e:
log.exception(e)
except Exception as error:
log.exception(error)
def image_url(self, name):
"""
......@@ -466,7 +468,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
bucket = conn.get_bucket(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["S3_BUCKET"])
key = Key(bucket)
key.key = "{}/{}".format(prefix, self.receipt_id);
key.key = "{}/{}".format(prefix, self.receipt_id)
return key
......@@ -507,7 +509,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
"Content-Type": "application/json",
"Date": formatdate(timeval=None, localtime=False, usegmt=True)
}
message, _, authorization = generate_signed_message(
_message, _sig, authorization = generate_signed_message(
"POST", headers, body, access_key, secret_key
)
headers['Authorization'] = authorization
......@@ -515,16 +517,18 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
return headers, body
def request_message_txt(self):
""" This is the body of the request we send across """
headers, body = self.create_request()
header_txt = "\n".join(
"{}: {}".format(h, v) for h,v in sorted(headers.items())
"{}: {}".format(h, v) for h, v in sorted(headers.items())
)
body_txt = json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8')
return header_txt + "\n\n" + body_txt
def send_request(self):
""" sends the request across to the endpoint """
headers, body = self.create_request()
response = requests.post(
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"],
......@@ -538,4 +542,4 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
log.debug("Return code: {}".format(response.status_code))
log.debug("Return message:\n\n{}\n\n".format(response.text))
return response
\ No newline at end of file
return response
......@@ -22,16 +22,11 @@ In case of PEM encoding, the private key can be encrypted with DES or 3TDES
according to a certain pass phrase. Only OpenSSL-compatible pass phrases are
supported.
"""
from collections import OrderedDict
from email.utils import formatdate
from hashlib import md5, sha256
from uuid import uuid4
import base64
import binascii
import json
import hmac
import logging
import sys
from Crypto import Random
from Crypto.Cipher import AES, PKCS1_OAEP
......@@ -39,12 +34,17 @@ from Crypto.PublicKey import RSA
log = logging.getLogger(__name__)
def encrypt_and_encode(data, key):
""" Encrypts and endcodes `data` using `key' """
return base64.urlsafe_b64encode(aes_encrypt(data, key))
def decode_and_decrypt(encoded_data, key):
""" Decrypts and decodes `data` using `key' """
return aes_decrypt(base64.urlsafe_b64decode(encoded_data), key)
def aes_encrypt(data, key):
"""
Return a version of the `data` that has been encrypted to
......@@ -53,11 +53,16 @@ def aes_encrypt(data, key):
padded_data = pad(data)
return cipher.encrypt(padded_data)
def aes_decrypt(encrypted_data, key):
"""
Decrypt `encrypted_data` using `key`
"""
cipher = aes_cipher_from_key(key)
padded_data = cipher.decrypt(encrypted_data)
return unpad(padded_data)
def aes_cipher_from_key(key):
"""
Given an AES key, return a Cipher object that has `encrypt()` and
......@@ -66,6 +71,7 @@ def aes_cipher_from_key(key):
"""
return AES.new(key, AES.MODE_CBC, generate_aes_iv(key))
def generate_aes_iv(key):
"""
Return the initialization vector Software Secure expects for a given AES
......@@ -73,17 +79,23 @@ def generate_aes_iv(key):
"""
return md5(key + md5(key).hexdigest()).hexdigest()[:AES.block_size]
def random_aes_key():
return Random.new().read(32)
def pad(data):
""" Pad the given `data` such that it fits into the proper AES block size """
bytes_to_pad = AES.block_size - len(data) % AES.block_size
return data + (bytes_to_pad * chr(bytes_to_pad))
def unpad(padded_data):
""" remove all padding from `padded_data` """
num_padded_bytes = ord(padded_data[-1])
return padded_data[:-num_padded_bytes]
def rsa_encrypt(data, rsa_pub_key_str):
"""
`rsa_pub_key` is a string with the public key
......@@ -93,11 +105,16 @@ def rsa_encrypt(data, rsa_pub_key_str):
encrypted_data = cipher.encrypt(data)
return encrypted_data
def rsa_decrypt(data, rsa_priv_key_str):
"""
When given some `data` and an RSA private key, decrypt the data
"""
key = RSA.importKey(rsa_priv_key_str)
cipher = PKCS1_OAEP.new(key)
return cipher.decrypt(data)
def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key):
"""
Given a message (either request or response), say whether it has a valid
......@@ -123,6 +140,7 @@ def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key)
return True
def generate_signed_message(method, headers_dict, body_dict, access_key, secret_key):
"""
Returns a (message, signature) pair.
......@@ -137,6 +155,7 @@ 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,
......@@ -149,6 +168,7 @@ def signing_format_message(method, headers_dict, body_dict):
return message
def header_string(headers_dict):
"""Given a dictionary of headers, return a canonical string representation."""
header_list = []
......@@ -160,7 +180,8 @@ def header_string(headers_dict):
if 'Content-MD5' in headers_dict:
header_list.append(headers_dict['Content-MD5'] + "\n")
return "".join(header_list) # Note that trailing \n's are important
return "".join(header_list) # Note that trailing \n's are important
def body_string(body_dict, prefix=""):
"""
......@@ -183,5 +204,5 @@ def body_string(body_dict, prefix=""):
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
return "".join(body_list) # Note that trailing \n's are important
......@@ -35,11 +35,4 @@ urlpatterns = patterns(
name="verify_student_results_callback",
),
url(
r'^show_verification_page/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
views.show_verification_page,
name="verify_student/show_verification_page"
),
)
"""
Views for the verification flow
"""
import json
......@@ -37,6 +37,12 @@ class VerifyView(View):
@method_decorator(login_required)
def get(self, request, course_id):
"""
Displays the main verification view, which contains three separate steps:
- Taking the standard face photo
- Taking the id photo
- Confirming that the photos and payment price are correct
before proceeding to payment
"""
# If the user has already been verified within the given time period,
# redirect straight to the payment -- no need to verify again.
......@@ -69,8 +75,8 @@ class VerifyView(View):
"user_full_name": request.user.profile.name,
"course_id": course_id,
"course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default,
"course_num" : course.display_number_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(),
"suggested_prices": [
decimal.Decimal(price)
......@@ -106,8 +112,8 @@ class VerifiedView(View):
context = {
"course_id": course_id,
"course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default,
"course_num" : course.display_number_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(),
"currency": verify_mode.currency.upper(),
"chosen_price": chosen_price,
......@@ -162,8 +168,9 @@ def create_order(request):
return HttpResponse(json.dumps(params), content_type="text/json")
@require_POST
@csrf_exempt # SS does its own message signing, and their API won't have a cookie value
@csrf_exempt # SS does its own message signing, and their API won't have a cookie value
def results_callback(request):
"""
Software Secure will call this callback to tell us whether a user is
......@@ -194,7 +201,7 @@ def results_callback(request):
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_SECRET_KEY"]
)
_, access_key_and_sig = headers["Authorization"].split(" ")
_response, access_key_and_sig = headers["Authorization"].split(" ")
access_key = access_key_and_sig.split(":")[0]
# This is what we should be doing...
......@@ -234,10 +241,11 @@ def results_callback(request):
return HttpResponse("OK!")
@login_required
def show_requirements(request, course_id):
"""
Show the requirements necessary for
Show the requirements necessary for the verification flow.
"""
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
return redirect(reverse('dashboard'))
......@@ -246,69 +254,8 @@ def show_requirements(request, course_id):
context = {
"course_id": course_id,
"course_name": course.display_name_with_default,
"course_org" : course.display_org_with_default,
"course_num" : course.display_number_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"is_not_active": not request.user.is_active,
}
return render_to_response("verify_student/show_requirements.html", context)
def show_verification_page(request):
pass
def enroll(user, course_id, mode_slug):
"""
Enroll the user in a course for a certain mode.
This is the view you send folks to when they click on the enroll button.
This does NOT cover changing enrollment modes -- it's intended for new
enrollments only, and will just redirect to the dashboard if it detects
that an enrollment already exists.
"""
# If the user is already enrolled, jump to the dashboard. Yeah, we could
# do upgrades here, but this method is complicated enough.
if CourseEnrollment.is_enrolled(user, course_id):
return HttpResponseRedirect(reverse('dashboard'))
available_modes = CourseModes.modes_for_course(course_id)
# If they haven't chosen a mode...
if not mode_slug:
# Does this course support multiple modes of Enrollment? If so, redirect
# to a page that lets them choose which mode they want.
if len(available_modes) > 1:
return HttpResponseRedirect(
reverse('choose_enroll_mode', kwargs={'course_id': course_id})
)
# Otherwise, we use the only mode that's supported...
else:
mode_slug = available_modes[0].slug
# If the mode is one of the simple, non-payment ones, do the enrollment and
# send them to their dashboard.
if mode_slug in ("honor", "audit"):
CourseEnrollment.enroll(user, course_id, mode=mode_slug)
return HttpResponseRedirect(reverse('dashboard'))
if mode_slug == "verify":
if SoftwareSecurePhotoVerification.has_submitted_recent_request(user):
# Capture payment info
# Create an order
# Create a VerifiedCertificate order item
return HttpResponse.Redirect(reverse('verified'))
# There's always at least one mode available (default is "honor"). If they
# haven't specified a mode, we just assume it's
if not mode:
mode = available_modes[0]
elif len(available_modes) == 1:
if mode != available_modes[0]:
raise Exception()
mode = available_modes[0]
if mode == "honor":
CourseEnrollment.enroll(user, course_id)
return HttpResponseRedirect(reverse('dashboard'))
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment