示例#1
0
    def _compare_signatures(self, secret_key, provider_id):
        """
        Compare signature we received with the signature we expect/have.

        Throw an error if they don't match.
        """

        data = self.initial_data
        actual_signature = data["signature"]

        # Accounts for old way of storing provider key
        if isinstance(secret_key, six.text_type) and signature(
                data, secret_key) != actual_signature:
            msg = 'Request from credit provider [{}] had an invalid signature.'.format(
                provider_id)
            raise PermissionDenied(msg)

        # Accounts for new way of storing provider key
        if isinstance(secret_key, list):
            # Received value just needs to match one of the keys we have
            key_match = False
            for secretvalue in secret_key:
                if signature(data, secretvalue) == actual_signature:
                    key_match = True

            if not key_match:
                msg = 'Request from credit provider [{}] had an invalid signature.'.format(
                    provider_id)
                raise PermissionDenied(msg)
示例#2
0
 def test_unicode_data(self):
     """ Verify the signature generation method supports Unicode data. """
     key = signature.get_shared_secret_key("asu")
     sig = signature.signature({'name': u'Ed Xavíer'}, key)
     self.assertEqual(
         sig,
         "76b6c9a657000829253d7c23977b35b34ad750c5681b524d7fdfb25cd5273cec")
示例#3
0
def _validate_signature(parameters, provider_id):
    """
    Check that the signature from the credit provider is valid.

    Arguments:
        parameters (dict): Parameters received from the credit provider.
        provider_id (unicode): Identifier for the credit provider.

    Returns:
        HttpResponseForbidden or None

    """
    secret_key = get_shared_secret_key(provider_id)
    if secret_key is None:
        log.error(
            (
                u'Could not retrieve secret key for credit provider with ID "%s".  '
                u'Since no key has been configured, we cannot validate requests from the credit provider.'
            ), provider_id
        )
        return HttpResponseForbidden("Credit provider credentials have not been configured.")

    if signature(parameters, secret_key) != parameters["signature"]:
        log.warning(u'Request from credit provider with ID "%s" had an invalid signature', parameters["signature"])
        return HttpResponseForbidden("Invalid signature.")
示例#4
0
def _validate_signature(parameters, provider_id):
    """
    Check that the signature from the credit provider is valid.

    Arguments:
        parameters (dict): Parameters received from the credit provider.
        provider_id (unicode): Identifier for the credit provider.

    Returns:
        HttpResponseForbidden or None

    """
    secret_key = get_shared_secret_key(provider_id)
    if secret_key is None:
        log.error((
            u'Could not retrieve secret key for credit provider with ID "%s".  '
            u'Since no key has been configured, we cannot validate requests from the credit provider.'
        ), provider_id)
        return HttpResponseForbidden(
            "Credit provider credentials have not been configured.")

    if signature(parameters, secret_key) != parameters["signature"]:
        log.warning(
            u'Request from credit provider with ID "%s" had an invalid signature',
            parameters["signature"])
        return HttpResponseForbidden("Invalid signature.")
示例#5
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.

        """
        provider_id = kwargs.get("provider_id", self.PROVIDER_ID)
        secret_key = kwargs.get("secret_key", TEST_CREDIT_PROVIDER_SECRET_KEY)
        timestamp = kwargs.get("timestamp", to_timestamp(datetime.datetime.now(pytz.UTC)))

        url = reverse("credit:provider_callback", args=[provider_id])

        parameters = {
            "request_uuid": request_uuid,
            "status": status,
            "timestamp": timestamp,
        }
        parameters["signature"] = kwargs.get("sig", signature(parameters, secret_key))

        return self.client.post(url, data=json.dumps(parameters), content_type=JSON)
示例#6
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.
            keys (dict): Override for CREDIT_PROVIDER_SECRET_KEYS setting.

        """
        provider_id = kwargs.get('provider_id', self.provider.provider_id)
        secret_key = kwargs.get('secret_key', '931433d583c84ca7ba41784bad3232e6')
        timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(pytz.UTC)))
        keys = kwargs.get('keys', {self.provider.provider_id: secret_key})

        url = reverse('credit:provider_callback', args=[provider_id])

        parameters = {
            'request_uuid': request_uuid,
            'status': status,
            'timestamp': timestamp,
        }
        parameters['signature'] = kwargs.get('sig', signature(parameters, secret_key))

        with override_settings(CREDIT_PROVIDER_SECRET_KEYS=keys):
            return self.client.post(url, data=json.dumps(parameters), content_type=JSON)
示例#7
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.
            keys (dict): Override for CREDIT_PROVIDER_SECRET_KEYS setting.

        """
        provider_id = kwargs.get('provider_id', self.provider.provider_id)
        secret_key = kwargs.get('secret_key', '931433d583c84ca7ba41784bad3232e6')
        timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(pytz.UTC)))
        keys = kwargs.get('keys', {self.provider.provider_id: secret_key})

        url = reverse('credit:provider_callback', args=[provider_id])

        parameters = {
            'request_uuid': request_uuid,
            'status': status,
            'timestamp': timestamp,
        }
        parameters['signature'] = kwargs.get('sig', signature(parameters, secret_key))

        with override_settings(CREDIT_PROVIDER_SECRET_KEYS=keys):
            return self.client.post(url, data=json.dumps(parameters), content_type=JSON)
示例#8
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.

        """
        provider_id = kwargs.get("provider_id", self.PROVIDER_ID)
        secret_key = kwargs.get("secret_key", TEST_CREDIT_PROVIDER_SECRET_KEY)
        timestamp = kwargs.get("timestamp",
                               to_timestamp(datetime.datetime.now(pytz.UTC)))

        url = reverse("credit:provider_callback", args=[provider_id])

        parameters = {
            "request_uuid": request_uuid,
            "status": status,
            "timestamp": timestamp,
        }
        parameters["signature"] = kwargs.get("sig",
                                             signature(parameters, secret_key))

        return self.client.post(url,
                                data=json.dumps(parameters),
                                content_type=JSON)
示例#9
0
 def test_unicode_secret_key(self):
     # Test a key that has type `unicode` but consists of ASCII characters
     # (This can happen, for example, when loading the key from a JSON configuration file)
     # When retrieving the shared secret, the type should be converted to `str`
     key = signature.get_shared_secret_key("asu")
     sig = signature.signature({}, key)
     assert sig == '7d70a26b834d9881cc14466eceac8d39188fc5ef5ffad9ab281a8327c2c0d093'
 def test_unicode_secret_key(self):
     # Test a key that has type `unicode` but consists of ASCII characters
     # (This can happen, for example, when loading the key from a JSON configuration file)
     # When retrieving the shared secret, the type should be converted to `str`
     key = signature.get_shared_secret_key("asu")
     sig = signature.signature({}, key)
     self.assertEqual(sig, "7d70a26b834d9881cc14466eceac8d39188fc5ef5ffad9ab281a8327c2c0d093")
示例#11
0
    def test_post_with_provider_integration(self):
        """ Verify the endpoint can create a new credit request. """
        username = self.user.username
        course = self.eligibility.course
        course_key = course.course_key
        final_grade = 0.95

        # Enable provider integration
        self.provider.enable_integration = True
        self.provider.save()

        # Add a single credit requirement (final grade)
        requirement = CreditRequirement.objects.create(
            course=course,
            namespace='grade',
            name='grade',
        )

        # Mark the user as having satisfied the requirement and eligible for credit.
        CreditRequirementStatus.objects.create(
            username=username,
            requirement=requirement,
            status='satisfied',
            reason={'final_grade': final_grade}
        )

        secret_key = 'secret'
        with override_settings(CREDIT_PROVIDER_SECRET_KEYS={self.provider.provider_id: secret_key}):
            response = self.post_credit_request(username, course_key)
        self.assertEqual(response.status_code, 200)

        # Check that the user's request status is pending
        request = CreditRequest.objects.get(username=username, course__course_key=course_key)
        self.assertEqual(request.status, 'pending')

        # Check request parameters
        content = json.loads(response.content)
        parameters = content['parameters']

        self.assertEqual(content['url'], self.provider.provider_url)
        self.assertEqual(content['method'], 'POST')
        self.assertEqual(len(parameters['request_uuid']), 32)
        self.assertEqual(parameters['course_org'], course_key.org)
        self.assertEqual(parameters['course_num'], course_key.course)
        self.assertEqual(parameters['course_run'], course_key.run)
        self.assertEqual(parameters['final_grade'], unicode(final_grade))
        self.assertEqual(parameters['user_username'], username)
        self.assertEqual(parameters['user_full_name'], self.user.get_full_name())
        self.assertEqual(parameters['user_mailing_address'], '')
        self.assertEqual(parameters['user_country'], '')

        # The signature is going to change each test run because the request
        # is assigned a different UUID each time.
        # For this reason, we use the signature function directly
        # (the "signature" parameter will be ignored when calculating the signature).
        # Other unit tests verify that the signature function is working correctly.
        self.assertEqual(parameters['signature'], signature(parameters, secret_key))
示例#12
0
    def test_compare_signatures_string_key(self):
        """ Verify compare_signature errors if string key does not match. """
        provider = CreditProviderFactory(
            provider_id='asu',
            active=False,
        )

        # Create a serializer that has a signature which was created with a key
        # that we do not have in our system.
        sig = signature.signature({}, 'iamthewrongkey')
        serializer = serializers.CreditProviderCallbackSerializer(
            data={'signature': sig})
        with pytest.raises(PermissionDenied):
            # The first arg here is key we have (that doesn't match the sig)
            serializer._compare_signatures('abcd1234', provider.provider_id)  # lint-amnesty, pylint: disable=protected-access
示例#13
0
    def validate_signature(self, value):
        """ Validate the signature and ensure the provider is setup properly. """
        provider_id = self.provider.provider_id
        secret_key = get_shared_secret_key(provider_id)
        if secret_key is None:
            msg = 'Could not retrieve secret key for credit provider [{}]. ' \
                  'Unable to validate requests from provider.'.format(provider_id)
            log.error(msg)
            raise PermissionDenied(msg)

        data = self.initial_data
        actual_signature = data["signature"]
        if signature(data, secret_key) != actual_signature:
            msg = 'Request from credit provider [{}] had an invalid signature.'.format(provider_id)
            raise PermissionDenied(msg)

        return value
示例#14
0
    def validate_signature(self, value):
        """ Validate the signature and ensure the provider is setup properly. """
        provider_id = self.provider.provider_id
        secret_key = get_shared_secret_key(provider_id)
        if secret_key is None:
            msg = 'Could not retrieve secret key for credit provider [{}]. ' \
                  'Unable to validate requests from provider.'.format(provider_id)
            log.error(msg)
            raise PermissionDenied(msg)

        data = self.initial_data
        actual_signature = data["signature"]
        if signature(data, secret_key) != actual_signature:
            msg = 'Request from credit provider [{}] had an invalid signature.'.format(provider_id)
            raise PermissionDenied(msg)

        return value
示例#15
0
    def test_credit_request_and_response(self):
        # Initiate a request
        response = self._create_credit_request(self.USERNAME, self.COURSE_KEY)
        self.assertEqual(response.status_code, 200)

        # Check that the user's request status is pending
        requests = api.get_credit_requests_for_user(self.USERNAME)
        self.assertEqual(len(requests), 1)
        self.assertEqual(requests[0]["status"], "pending")

        # Check request parameters
        content = json.loads(response.content)
        self.assertEqual(content["url"], self.PROVIDER_URL)
        self.assertEqual(content["method"], "POST")
        self.assertEqual(len(content["parameters"]["request_uuid"]), 32)
        self.assertEqual(content["parameters"]["course_org"], "edX")
        self.assertEqual(content["parameters"]["course_num"], "DemoX")
        self.assertEqual(content["parameters"]["course_run"], "Demo_Course")
        self.assertEqual(content["parameters"]["final_grade"], self.FINAL_GRADE)
        self.assertEqual(content["parameters"]["user_username"], self.USERNAME)
        self.assertEqual(content["parameters"]["user_full_name"], self.USER_FULL_NAME)
        self.assertEqual(content["parameters"]["user_mailing_address"], "")
        self.assertEqual(content["parameters"]["user_country"], "")

        # The signature is going to change each test run because the request
        # is assigned a different UUID each time.
        # For this reason, we use the signature function directly
        # (the "signature" parameter will be ignored when calculating the signature).
        # Other unit tests verify that the signature function is working correctly.
        self.assertEqual(
            content["parameters"]["signature"],
            signature(content["parameters"], TEST_CREDIT_PROVIDER_SECRET_KEY)
        )

        # Simulate a response from the credit provider
        response = self._credit_provider_callback(
            content["parameters"]["request_uuid"],
            "approved"
        )
        self.assertEqual(response.status_code, 200)

        # Check that the user's status is approved
        requests = api.get_credit_requests_for_user(self.USERNAME)
        self.assertEqual(len(requests), 1)
        self.assertEqual(requests[0]["status"], "approved")
示例#16
0
    def test_credit_request_and_response(self):
        # Initiate a request
        response = self._create_credit_request(self.USERNAME, self.COURSE_KEY)
        self.assertEqual(response.status_code, 200)

        # Check that the user's request status is pending
        requests = api.get_credit_requests_for_user(self.USERNAME)
        self.assertEqual(len(requests), 1)
        self.assertEqual(requests[0]["status"], "pending")

        # Check request parameters
        content = json.loads(response.content)
        self.assertEqual(content["url"], self.PROVIDER_URL)
        self.assertEqual(content["method"], "POST")
        self.assertEqual(len(content["parameters"]["request_uuid"]), 32)
        self.assertEqual(content["parameters"]["course_org"], "edX")
        self.assertEqual(content["parameters"]["course_num"], "DemoX")
        self.assertEqual(content["parameters"]["course_run"], "Demo_Course")
        self.assertEqual(content["parameters"]["final_grade"], self.FINAL_GRADE)
        self.assertEqual(content["parameters"]["user_username"], self.USERNAME)
        self.assertEqual(content["parameters"]["user_full_name"], self.USER_FULL_NAME)
        self.assertEqual(content["parameters"]["user_mailing_address"], "")
        self.assertEqual(content["parameters"]["user_country"], "")

        # The signature is going to change each test run because the request
        # is assigned a different UUID each time.
        # For this reason, we use the signature function directly
        # (the "signature" parameter will be ignored when calculating the signature).
        # Other unit tests verify that the signature function is working correctly.
        self.assertEqual(
            content["parameters"]["signature"],
            signature(content["parameters"], TEST_CREDIT_PROVIDER_SECRET_KEY)
        )

        # Simulate a response from the credit provider
        response = self._credit_provider_callback(
            content["parameters"]["request_uuid"],
            "approved"
        )
        self.assertEqual(response.status_code, 200)

        # Check that the user's status is approved
        requests = api.get_credit_requests_for_user(self.USERNAME)
        self.assertEqual(len(requests), 1)
        self.assertEqual(requests[0]["status"], "approved")
示例#17
0
    def test_compare_signatures_list_key(self):
        """
        Verify compare_signature errors if no keys that are stored in the list
        in config match the key handed in the signature.
        """
        provider = CreditProviderFactory(
            provider_id='asu',
            active=False,
        )

        sig = signature.signature({}, 'iamthewrongkey')
        serializer = serializers.CreditProviderCallbackSerializer(
            data={'signature': sig})

        with pytest.raises(PermissionDenied):
            # The first arg here is the list of keys he have (that dont matcht the sig)
            serializer._compare_signatures(  # lint-amnesty, pylint: disable=protected-access
                ['abcd1234', 'xyz789'], provider.provider_id)
示例#18
0
    def test_post_with_provider_integration(self):
        """ Verify the endpoint can create a new credit request. """
        username = self.user.username
        course = self.eligibility.course
        course_key = course.course_key
        final_grade = 0.95

        # Enable provider integration
        self.provider.enable_integration = True
        self.provider.save()

        # Add a single credit requirement (final grade)
        requirement = CreditRequirement.objects.create(
            course=course,
            namespace='grade',
            name='grade',
        )

        # Mark the user as having satisfied the requirement and eligible for credit.
        CreditRequirementStatus.objects.create(
            username=username,
            requirement=requirement,
            status='satisfied',
            reason={'final_grade': final_grade}
        )

        secret_key = 'secret'
        # Provider keys can be stored as a string or list of strings
        secret_key_with_key_as_string = {self.provider.provider_id: secret_key}
        # The None represents a key that was not ascii encodable
        secret_key_with_key_as_list = {
            self.provider.provider_id: [secret_key, None]
        }

        for secret_key_dict in [secret_key_with_key_as_string, secret_key_with_key_as_list]:
            with override_settings(CREDIT_PROVIDER_SECRET_KEYS=secret_key_dict):
                response = self.post_credit_request(username, course_key)
            assert response.status_code == 200

            # Check that the user's request status is pending
            request = CreditRequest.objects.get(username=username, course__course_key=course_key)
            assert request.status == 'pending'

            # Check request parameters
            content = json.loads(response.content.decode('utf-8'))
            parameters = content['parameters']

            assert content['url'] == self.provider.provider_url
            assert content['method'] == 'POST'
            assert len(parameters['request_uuid']) == 32
            assert parameters['course_org'] == course_key.org
            assert parameters['course_num'] == course_key.course
            assert parameters['course_run'] == course_key.run
            assert parameters['final_grade'] == six.text_type(final_grade)
            assert parameters['user_username'] == username
            assert parameters['user_full_name'] == self.user.get_full_name()
            assert parameters['user_mailing_address'] == ''
            assert parameters['user_country'] == ''

            # The signature is going to change each test run because the request
            # is assigned a different UUID each time.
            # For this reason, we use the signature function directly
            # (the "signature" parameter will be ignored when calculating the signature).
            # Other unit tests verify that the signature function is working correctly.
            assert parameters['signature'] == signature(parameters, secret_key)
示例#19
0
def create_credit_request(course_key, provider_id, username):
    """
    Initiate a request for credit from a credit provider.

    This will return the parameters that the user's browser will need to POST
    to the credit provider.  It does NOT calculate the signature.

    Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests.

    A provider can be configured either with *integration enabled* or not.
    If automatic integration is disabled, this method will simply return
    a URL to the credit provider and method set to "GET", so the student can
    visit the URL and request credit directly.  No database record will be created
    to track these requests.

    If automatic integration *is* enabled, then this will also return the parameters
    that the user's browser will need to POST to the credit provider.
    These parameters will be digitally signed using a secret key shared with the credit provider.

    A database record will be created to track the request with a 32-character UUID.
    The returned dictionary can be used by the user's browser to send a POST request to the credit provider.

    If a pending request already exists, this function should return a request description with the same UUID.
    (Other parameters, such as the user's full name may be different than the original request).

    If a completed request (either accepted or rejected) already exists, this function will
    raise an exception.  Users are not allowed to make additional requests once a request
    has been completed.

    Arguments:
        course_key (CourseKey): The identifier for the course.
        provider_id (str): The identifier of the credit provider.
        username (str): The user initiating the request.

    Returns: dict

    Raises:
        UserIsNotEligible: The user has not satisfied eligibility requirements for credit.
        CreditProviderNotConfigured: The credit provider has not been configured for this course.
        RequestAlreadyCompleted: The user has already submitted a request and received a response
            from the credit provider.

    Example Usage:
        >>> create_credit_request(course.id, "hogwarts", "ron")
        {
            "url": "https://credit.example.com/request",
            "method": "POST",
            "parameters": {
                "request_uuid": "557168d0f7664fe59097106c67c3f847",
                "timestamp": 1434631630,
                "course_org": "HogwartsX",
                "course_num": "Potions101",
                "course_run": "1T2015",
                "final_grade": "0.95",
                "user_username": "******",
                "user_email": "*****@*****.**",
                "user_full_name": "Ron Weasley",
                "user_mailing_address": "",
                "user_country": "US",
                "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI="
            }
        }

    """
    try:
        user_eligibility = CreditEligibility.objects.select_related(
            'course').get(username=username, course__course_key=course_key)
        credit_course = user_eligibility.course
        credit_provider = CreditProvider.objects.get(provider_id=provider_id)
    except CreditEligibility.DoesNotExist:
        log.warning(
            u'User "%s" tried to initiate a request for credit in course "%s", '
            u'but the user is not eligible for credit', username, course_key)
        raise UserIsNotEligible
    except CreditProvider.DoesNotExist:
        log.error(u'Credit provider with ID "%s" has not been configured.',
                  provider_id)
        raise CreditProviderNotConfigured

    # Check if we've enabled automatic integration with the credit
    # provider.  If not, we'll show the user a link to a URL
    # where the user can request credit directly from the provider.
    # Note that we do NOT track these requests in our database,
    # since the state would always be "pending" (we never hear back).
    if not credit_provider.enable_integration:
        return {
            "url": credit_provider.provider_url,
            "method": "GET",
            "parameters": {}
        }
    else:
        # If automatic credit integration is enabled, then try
        # to retrieve the shared signature *before* creating the request.
        # That way, if there's a misconfiguration, we won't have requests
        # in our system that we know weren't sent to the provider.
        shared_secret_key = get_shared_secret_key(credit_provider.provider_id)
        if shared_secret_key is None:
            msg = u'Credit provider with ID "{provider_id}" does not have a secret key configured.'.format(
                provider_id=credit_provider.provider_id)
            log.error(msg)
            raise CreditProviderNotConfigured(msg)

    # Initiate a new request if one has not already been created
    credit_request, created = CreditRequest.objects.get_or_create(
        course=credit_course,
        provider=credit_provider,
        username=username,
    )

    # Check whether we've already gotten a response for a request,
    # If so, we're not allowed to issue any further requests.
    # Skip checking the status if we know that we just created this record.
    if not created and credit_request.status != "pending":
        log.warning((
            u'Cannot initiate credit request because the request with UUID "%s" '
            u'exists with status "%s"'), credit_request.uuid,
                    credit_request.status)
        raise RequestAlreadyCompleted

    if created:
        credit_request.uuid = uuid.uuid4().hex

    # Retrieve user account and profile info
    user = User.objects.select_related('profile').get(username=username)

    # Retrieve the final grade from the eligibility table
    try:
        final_grade = CreditRequirementStatus.objects.get(
            username=username,
            requirement__namespace="grade",
            requirement__name="grade",
            requirement__course__course_key=course_key,
            status="satisfied").reason["final_grade"]

        # NOTE (CCB): Limiting the grade to seven characters is a hack for ASU.
        if len(unicode(final_grade)) > 7:
            final_grade = u'{:.5f}'.format(final_grade)
        else:
            final_grade = unicode(final_grade)

    except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
        log.exception(
            "Could not retrieve final grade from the credit eligibility table "
            "for user %s in course %s.", user.id, course_key)
        raise UserIsNotEligible

    parameters = {
        "request_uuid":
        credit_request.uuid,
        "timestamp":
        to_timestamp(datetime.datetime.now(pytz.UTC)),
        "course_org":
        course_key.org,
        "course_num":
        course_key.course,
        "course_run":
        course_key.run,
        "final_grade":
        final_grade,
        "user_username":
        user.username,
        "user_email":
        user.email,
        "user_full_name":
        user.profile.name,
        "user_mailing_address":
        "",
        "user_country": (user.profile.country.code
                         if user.profile.country.code is not None else ""),
    }

    credit_request.parameters = parameters
    credit_request.save()

    if created:
        log.info(u'Created new request for credit with UUID "%s"',
                 credit_request.uuid)
    else:
        log.info(
            u'Updated request for credit with UUID "%s" so the user can re-issue the request',
            credit_request.uuid)

    # Sign the parameters using a secret key we share with the credit provider.
    parameters["signature"] = signature(parameters, shared_secret_key)

    return {
        "url": credit_provider.provider_url,
        "method": "POST",
        "parameters": parameters
    }
示例#20
0
def create_credit_request(course_key, provider_id, username):
    """
    Initiate a request for credit from a credit provider.

    This will return the parameters that the user's browser will need to POST
    to the credit provider.  It does NOT calculate the signature.

    Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests.

    A provider can be configured either with *integration enabled* or not.
    If automatic integration is disabled, this method will simply return
    a URL to the credit provider and method set to "GET", so the student can
    visit the URL and request credit directly.  No database record will be created
    to track these requests.

    If automatic integration *is* enabled, then this will also return the parameters
    that the user's browser will need to POST to the credit provider.
    These parameters will be digitally signed using a secret key shared with the credit provider.

    A database record will be created to track the request with a 32-character UUID.
    The returned dictionary can be used by the user's browser to send a POST request to the credit provider.

    If a pending request already exists, this function should return a request description with the same UUID.
    (Other parameters, such as the user's full name may be different than the original request).

    If a completed request (either accepted or rejected) already exists, this function will
    raise an exception.  Users are not allowed to make additional requests once a request
    has been completed.

    Arguments:
        course_key (CourseKey): The identifier for the course.
        provider_id (str): The identifier of the credit provider.
        user (User): The user initiating the request.

    Returns: dict

    Raises:
        UserIsNotEligible: The user has not satisfied eligibility requirements for credit.
        CreditProviderNotConfigured: The credit provider has not been configured for this course.
        RequestAlreadyCompleted: The user has already submitted a request and received a response
            from the credit provider.

    Example Usage:
        >>> create_credit_request(course.id, "hogwarts", "ron")
        {
            "url": "https://credit.example.com/request",
            "method": "POST",
            "parameters": {
                "request_uuid": "557168d0f7664fe59097106c67c3f847",
                "timestamp": 1434631630,
                "course_org": "HogwartsX",
                "course_num": "Potions101",
                "course_run": "1T2015",
                "final_grade": 0.95,
                "user_username": "******",
                "user_email": "*****@*****.**",
                "user_full_name": "Ron Weasley",
                "user_mailing_address": "",
                "user_country": "US",
                "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI="
            }
        }

    """
    try:
        user_eligibility = CreditEligibility.objects.select_related('course').get(
            username=username,
            course__course_key=course_key
        )
        credit_course = user_eligibility.course
        credit_provider = CreditProvider.objects.get(provider_id=provider_id)
    except CreditEligibility.DoesNotExist:
        log.warning(
            u'User "%s" tried to initiate a request for credit in course "%s", '
            u'but the user is not eligible for credit',
            username, course_key
        )
        raise UserIsNotEligible
    except CreditProvider.DoesNotExist:
        log.error(u'Credit provider with ID "%s" has not been configured.', provider_id)
        raise CreditProviderNotConfigured

    # Check if we've enabled automatic integration with the credit
    # provider.  If not, we'll show the user a link to a URL
    # where the user can request credit directly from the provider.
    # Note that we do NOT track these requests in our database,
    # since the state would always be "pending" (we never hear back).
    if not credit_provider.enable_integration:
        return {
            "url": credit_provider.provider_url,
            "method": "GET",
            "parameters": {}
        }
    else:
        # If automatic credit integration is enabled, then try
        # to retrieve the shared signature *before* creating the request.
        # That way, if there's a misconfiguration, we won't have requests
        # in our system that we know weren't sent to the provider.
        shared_secret_key = get_shared_secret_key(credit_provider.provider_id)
        if shared_secret_key is None:
            msg = u'Credit provider with ID "{provider_id}" does not have a secret key configured.'.format(
                provider_id=credit_provider.provider_id
            )
            log.error(msg)
            raise CreditProviderNotConfigured(msg)

    # Initiate a new request if one has not already been created
    credit_request, created = CreditRequest.objects.get_or_create(
        course=credit_course,
        provider=credit_provider,
        username=username,
    )

    # Check whether we've already gotten a response for a request,
    # If so, we're not allowed to issue any further requests.
    # Skip checking the status if we know that we just created this record.
    if not created and credit_request.status != "pending":
        log.warning(
            (
                u'Cannot initiate credit request because the request with UUID "%s" '
                u'exists with status "%s"'
            ), credit_request.uuid, credit_request.status
        )
        raise RequestAlreadyCompleted

    if created:
        credit_request.uuid = uuid.uuid4().hex

    # Retrieve user account and profile info
    user = User.objects.select_related('profile').get(username=username)

    # Retrieve the final grade from the eligibility table
    try:
        final_grade = CreditRequirementStatus.objects.get(
            username=username,
            requirement__namespace="grade",
            requirement__name="grade",
            status="satisfied"
        ).reason["final_grade"]
    except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
        log.exception(
            "Could not retrieve final grade from the credit eligibility table "
            "for user %s in course %s.",
            user.id, course_key
        )
        raise UserIsNotEligible

    parameters = {
        "request_uuid": credit_request.uuid,
        "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)),
        "course_org": course_key.org,
        "course_num": course_key.course,
        "course_run": course_key.run,
        "final_grade": final_grade,
        "user_username": user.username,
        "user_email": user.email,
        "user_full_name": user.profile.name,
        "user_mailing_address": (
            user.profile.mailing_address
            if user.profile.mailing_address is not None
            else ""
        ),
        "user_country": (
            user.profile.country.code
            if user.profile.country.code is not None
            else ""
        ),
    }

    credit_request.parameters = parameters
    credit_request.save()

    if created:
        log.info(u'Created new request for credit with UUID "%s"', credit_request.uuid)
    else:
        log.info(
            u'Updated request for credit with UUID "%s" so the user can re-issue the request',
            credit_request.uuid
        )

    # Sign the parameters using a secret key we share with the credit provider.
    parameters["signature"] = signature(parameters, shared_secret_key)

    return {
        "url": credit_provider.provider_url,
        "method": "POST",
        "parameters": parameters
    }
 def test_unicode_data(self):
     """ Verify the signature generation method supports Unicode data. """
     key = signature.get_shared_secret_key("asu")
     sig = signature.signature({'name': u'Ed Xavíer'}, key)
     self.assertEqual(sig, "76b6c9a657000829253d7c23977b35b34ad750c5681b524d7fdfb25cd5273cec")
示例#22
0
    def test_post_with_provider_integration(self):
        """ Verify the endpoint can create a new credit request. """
        username = self.user.username
        course = self.eligibility.course
        course_key = course.course_key
        final_grade = 0.95

        # Enable provider integration
        self.provider.enable_integration = True
        self.provider.save()

        # Add a single credit requirement (final grade)
        requirement = CreditRequirement.objects.create(
            course=course,
            namespace='grade',
            name='grade',
        )

        # Mark the user as having satisfied the requirement and eligible for credit.
        CreditRequirementStatus.objects.create(
            username=username,
            requirement=requirement,
            status='satisfied',
            reason={'final_grade': final_grade})

        secret_key = 'secret'
        with override_settings(CREDIT_PROVIDER_SECRET_KEYS={
                self.provider.provider_id: secret_key
        }):
            response = self.post_credit_request(username, course_key)
        self.assertEqual(response.status_code, 200)

        # Check that the user's request status is pending
        request = CreditRequest.objects.get(username=username,
                                            course__course_key=course_key)
        self.assertEqual(request.status, 'pending')

        # Check request parameters
        content = json.loads(response.content)
        parameters = content['parameters']

        self.assertEqual(content['url'], self.provider.provider_url)
        self.assertEqual(content['method'], 'POST')
        self.assertEqual(len(parameters['request_uuid']), 32)
        self.assertEqual(parameters['course_org'], course_key.org)
        self.assertEqual(parameters['course_num'], course_key.course)
        self.assertEqual(parameters['course_run'], course_key.run)
        self.assertEqual(parameters['final_grade'], six.text_type(final_grade))
        self.assertEqual(parameters['user_username'], username)
        self.assertEqual(parameters['user_full_name'],
                         self.user.get_full_name())
        self.assertEqual(parameters['user_mailing_address'], '')
        self.assertEqual(parameters['user_country'], '')

        # The signature is going to change each test run because the request
        # is assigned a different UUID each time.
        # For this reason, we use the signature function directly
        # (the "signature" parameter will be ignored when calculating the signature).
        # Other unit tests verify that the signature function is working correctly.
        self.assertEqual(parameters['signature'],
                         signature(parameters, secret_key))