Пример #1
0
 def test_data_sharing_consent_requirement(self, mock_get_ec):
     """
     Test that we get the correct requirement string for the current consent statae.
     """
     request = mock.MagicMock(session={"partial_pipeline": "thing"})
     mock_ec = mock.MagicMock(
         enforces_data_sharing_consent=mock.MagicMock(return_value=True), requests_data_sharing_consent=True
     )
     mock_get_ec.return_value = mock_ec
     self.assertEqual(data_sharing_consent_requirement_at_login(request), "required")
     mock_ec.enforces_data_sharing_consent.return_value = False
     self.assertEqual(data_sharing_consent_requirement_at_login(request), "optional")
     mock_ec.requests_data_sharing_consent = False
     self.assertEqual(data_sharing_consent_requirement_at_login(request), None)
 def test_data_sharing_consent_requirement(self, mock_get_ec):
     """
     Test that we get the correct requirement string for the current consent statae.
     """
     request = mock.MagicMock(session={'partial_pipeline': 'thing'})
     mock_ec = mock.MagicMock(
         enforces_data_sharing_consent=mock.MagicMock(return_value=True),
         requests_data_sharing_consent=True,
     )
     mock_get_ec.return_value = mock_ec
     self.assertEqual(data_sharing_consent_requirement_at_login(request),
                      'required')
     mock_ec.enforces_data_sharing_consent.return_value = False
     self.assertEqual(data_sharing_consent_requirement_at_login(request),
                      'optional')
     mock_ec.requests_data_sharing_consent = False
     self.assertEqual(data_sharing_consent_requirement_at_login(request),
                      None)
 def test_utils_with_enterprise_disabled(self, mock_enterprise_enabled):
     """
     Test that the enterprise app not being available causes
     the utilities to return the expected default values.
     """
     mock_enterprise_enabled.return_value = False
     self.assertFalse(data_sharing_consent_requested(None))
     self.assertFalse(data_sharing_consent_required_at_login(None))
     self.assertEqual(data_sharing_consent_requirement_at_login(None), None)
     self.assertEqual(insert_enterprise_fields(None, None), None)
     self.assertEqual(insert_enterprise_pipeline_elements(None), None)
Пример #4
0
 def test_utils_with_enterprise_disabled(self, mock_enterprise_enabled):
     """
     Test that the enterprise app not being available causes
     the utilities to return the expected default values.
     """
     mock_enterprise_enabled.return_value = False
     self.assertFalse(data_sharing_consent_requested(None))
     self.assertFalse(data_sharing_consent_required_at_login(None))
     self.assertEqual(data_sharing_consent_requirement_at_login(None), None)
     self.assertEqual(insert_enterprise_fields(None, None), None)
     self.assertEqual(insert_enterprise_pipeline_elements(None), None)
Пример #5
0
def create_account_with_params_custom(request, params):
    """
    Given a request and a dict of parameters (which may or may not have come
    from the request), create an account for the requesting user, including
    creating a comments service user object and sending an activation email.
    This also takes external/third-party auth into account, updates that as
    necessary, and authenticates the user for the request's session.

    Does not return anything.

    Raises AccountValidationError if an account with the username or email
    specified by params already exists, or ValidationError if any of the given
    parameters is invalid for any other reason.

    Issues with this code:
    * It is not transactional. If there is a failure part-way, an incomplete
      account will be created and left in the database.
    * Third-party auth passwords are not verified. There is a comment that
      they are unused, but it would be helpful to have a sanity check that
      they are sane.
    * It is over 300 lines long (!) and includes disprate functionality, from
      registration e-mails to all sorts of other things. It should be broken
      up into semantically meaningful functions.
    * The user-facing text is rather unfriendly (e.g. "Username must be a
      minimum of two characters long" rather than "Please use a username of
      at least two characters").
    """
    # Copy params so we can modify it; we can't just do dict(params) because if
    # params is request.POST, that results in a dict containing lists of values
    params = dict(params.items())

    # allow to define custom set of required/optional/hidden fields via configuration
    extra_fields = configuration_helpers.get_value(
        'REGISTRATION_EXTRA_FIELDS',
        getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {}))

    # Boolean of whether a 3rd party auth provider and credentials were provided in
    # the API so the newly created account can link with the 3rd party account.
    #
    # Note: this is orthogonal to the 3rd party authentication pipeline that occurs
    # when the account is created via the browser and redirect URLs.
    should_link_with_social_auth = third_party_auth.is_enabled(
    ) and 'provider' in params

    if should_link_with_social_auth or (third_party_auth.is_enabled()
                                        and pipeline.running(request)):
        params["password"] = pipeline.make_random_password()

    # Add a form requirement for data sharing consent if the EnterpriseCustomer
    # for the request requires it at login
    extra_fields[
        'data_sharing_consent'] = data_sharing_consent_requirement_at_login(
            request)

    # if doing signup for an external authorization, then get email, password, name from the eamap
    # don't use the ones from the form, since the user could have hacked those
    # unless originally we didn't get a valid email or name from the external auth
    # TODO: We do not check whether these values meet all necessary criteria, such as email length
    do_external_auth = 'ExternalAuthMap' in request.session
    if do_external_auth:
        eamap = request.session['ExternalAuthMap']
        try:
            validate_email(eamap.external_email)
            params["email"] = eamap.external_email
        except ValidationError:
            pass
        if eamap.external_name.strip() != '':
            params["name"] = eamap.external_name
        params["password"] = eamap.internal_password
        log.debug(u'In create_account with external_auth: user = %s, email=%s',
                  params["name"], params["email"])

    extended_profile_fields = configuration_helpers.get_value(
        'extended_profile_fields', [])
    enforce_password_policy = (settings.FEATURES.get(
        "ENFORCE_PASSWORD_POLICY", False) and not do_external_auth)
    # Can't have terms of service for certain SHIB users, like at Stanford
    registration_fields = getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})
    tos_required = (registration_fields.get('terms_of_service') != 'hidden'
                    or registration_fields.get('honor_code') != 'hidden') and (
                        not settings.FEATURES.get("AUTH_USE_SHIB")
                        or not settings.FEATURES.get("SHIB_DISABLE_TOS")
                        or not do_external_auth
                        or not eamap.external_domain.startswith(
                            openedx.core.djangoapps.external_auth.views.
                            SHIBBOLETH_DOMAIN_PREFIX))

    params['name'] = "{} {}".format(params.get('first_name'),
                                    params.get('last_name'))
    form = AccountCreationForm(
        data=params,
        extra_fields=extra_fields,
        extended_profile_fields=extended_profile_fields,
        enforce_username_neq_password=True,
        enforce_password_policy=enforce_password_policy,
        tos_required=tos_required,
    )
    custom_form = get_registration_extension_form(data=params)

    # Perform operations within a transaction that are critical to account creation
    with transaction.atomic():
        # first, create the account
        (user, profile,
         registration) = _do_create_account_custom(form, custom_form)

        # next, link the account with social auth, if provided via the API.
        # (If the user is using the normal register page, the social auth pipeline does the linking, not this code)
        if should_link_with_social_auth:
            backend_name = params['provider']
            request.social_strategy = social_utils.load_strategy(request)
            redirect_uri = reverse('social:complete', args=(backend_name, ))
            request.backend = social_utils.load_backend(
                request.social_strategy, backend_name, redirect_uri)
            social_access_token = params.get('access_token')
            if not social_access_token:
                raise ValidationError({
                    'access_token': [
                        _("An access_token is required when passing value ({}) for provider."
                          ).format(params['provider'])
                    ]
                })
            request.session[
                pipeline.AUTH_ENTRY_KEY] = pipeline.AUTH_ENTRY_REGISTER_API
            pipeline_user = None
            error_message = ""
            try:
                pipeline_user = request.backend.do_auth(social_access_token,
                                                        user=user)
            except AuthAlreadyAssociated:
                error_message = _(
                    "The provided access_token is already associated with another user."
                )
            except (HTTPError, AuthException):
                error_message = _("The provided access_token is not valid.")
            if not pipeline_user or not isinstance(pipeline_user, User):
                # Ensure user does not re-enter the pipeline
                request.social_strategy.clean_partial_pipeline()
                raise ValidationError({'access_token': [error_message]})

    # Perform operations that are non-critical parts of account creation
    preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language())

    if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'):
        try:
            enable_notifications(user)
        except Exception:  # pylint: disable=broad-except
            log.exception(
                "Enable discussion notifications failed for user {id}.".format(
                    id=user.id))

    dog_stats_api.increment("common.student.account_created")

    # If the user is registering via 3rd party auth, track which provider they use
    third_party_provider = None
    running_pipeline = None
    if third_party_auth.is_enabled() and pipeline.running(request):
        running_pipeline = pipeline.get(request)
        third_party_provider = provider.Registry.get_from_pipeline(
            running_pipeline)
        # Store received data sharing consent field values in the pipeline for use
        # by any downstream pipeline elements which require them.
        running_pipeline['kwargs'][
            'data_sharing_consent'] = form.cleaned_data.get(
                'data_sharing_consent', None)

    # Track the user's registration
    if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
        tracking_context = tracker.get_tracker().resolve_context()
        identity_args = [
            user.id,  # pylint: disable=no-member
            {
                'email':
                user.email,
                'username':
                user.username,
                'name':
                profile.name,
                # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
                'age':
                profile.age or -1,
                'yearOfBirth':
                profile.year_of_birth or datetime.datetime.now(UTC).year,
                'education':
                profile.level_of_education_display,
                'address':
                profile.mailing_address,
                'gender':
                profile.gender_display,
                'country':
                unicode(profile.country),
            }
        ]

        if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'):
            identity_args.append(
                {"MailChimp": {
                    "listId": settings.MAILCHIMP_NEW_USER_LIST_ID
                }})

        analytics.identify(*identity_args)

        analytics.track(
            user.id,
            "edx.bi.user.account.registered", {
                'category':
                'conversion',
                'label':
                params.get('course_id'),
                'provider':
                third_party_provider.name if third_party_provider else None
            },
            context={
                'ip': tracking_context.get('ip'),
                'Google Analytics': {
                    'clientId': tracking_context.get('client_id')
                }
            })

    # Announce registration
    REGISTER_USER.send(sender=None, user=user, profile=profile)

    create_comments_service_user(user)

    # Don't send email if we are:
    #
    # 1. Doing load testing.
    # 2. Random user generation for other forms of testing.
    # 3. External auth bypassing activation.
    # 4. Have the platform configured to not require e-mail activation.
    # 5. Registering a new user using a trusted third party provider (with skip_email_verification=True)
    #
    # Note that this feature is only tested as a flag set one way or
    # the other for *new* systems. we need to be careful about
    # changing settings on a running system to make sure no users are
    # left in an inconsistent state (or doing a migration if they are).
    send_email = (
        not settings.FEATURES.get('SKIP_EMAIL_VALIDATION', None)
        and not settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')
        and not (do_external_auth and
                 settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'))
        and
        not (third_party_provider
             and third_party_provider.skip_email_verification and user.email
             == running_pipeline['kwargs'].get('details', {}).get('email')))
    if send_email:
        send_account_activation_email(request, registration, user)
    else:
        registration.activate()
        _enroll_user_in_pending_courses(
            user)  # Enroll student in any pending courses

    # Immediately after a user creates an account, we log them in. They are only
    # logged in until they close the browser. They can't log in again until they click
    # the activation link from the email.
    new_user = authenticate(username=user.username,
                            password=params['password'])
    login(request, new_user)
    request.session.set_expiry(0)

    try:
        record_registration_attributions(request, new_user)
    # Don't prevent a user from registering due to attribution errors.
    except Exception:  # pylint: disable=broad-except
        log.exception('Error while attributing cookies to user registration.')

    # TODO: there is no error checking here to see that the user actually logged in successfully,
    # and is not yet an active user.
    if new_user is not None:
        AUDIT_LOG.info(u"Login success on new account creation - {0}".format(
            new_user.username))

    if do_external_auth:
        eamap.user = new_user
        eamap.dtsignup = datetime.datetime.now(UTC)
        eamap.save()
        AUDIT_LOG.info(u"User registered with external_auth %s",
                       new_user.username)
        AUDIT_LOG.info(u'Updated ExternalAuthMap for %s to be %s',
                       new_user.username, eamap)

        if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
            log.info('bypassing activation email')
            new_user.is_active = True
            new_user.save()
            AUDIT_LOG.info(
                u"Login activated on extauth account - {0} ({1})".format(
                    new_user.username, new_user.email))

    return new_user