def test_get_setting_provided_settings(self):
     """
     Ensure the provided values for settings are used
     when present
     """
     self.assertEqual(get_setting("LOGIN_URL"), "/new/login/")
     self.assertEqual(get_setting("UNIAUTH_LOGIN_DISPLAY_STANDARD"), False)
     self.assertEqual(get_setting("UNIAUTH_MAX_LINKED_EMAILS"), -11)
     # Should still be the default value, since it wasn't set
     self.assertEqual(get_setting("PASSWORD_RESET_TIMEOUT_DAYS"), 3)
 def test_get_setting_default_settings(self):
     """
     Ensure the default values for Uniauth's settings are
     returned when not provided in the settings file
     """
     # The test settings file does not set any Uniauth
     # settings - so everything should be the default value
     self.assertEqual(get_setting("LOGIN_URL"), "/accounts/login/")
     self.assertEqual(get_setting("PASSWORD_RESET_TIMEOUT_DAYS"), 3)
     self.assertEqual(get_setting("UNIAUTH_LOGIN_DISPLAY_STANDARD"), True)
     for setting, default_value in DEFAULT_SETTING_VALUES.items():
         self.assertEqual(get_setting(setting), default_value)
Beispiel #3
0
def logout(request):
    """
    Logs the user out of their Uniauth account, and
    redirects to the next page, defaulting to the URL
    specified by the UNIAUTH_LOGOUT_REDIRECT_URL setting.

    If no redirect page is set (URL parameter not given
    and UNIAUTH_LOGOUT_REDIRECT_URL is None), renders the
    logout template.

    Also logs the user out of CAS if they logged in
    via CAS, and the UNIAUTH_LOGOUT_CAS_COMPLETELY
    setting is true.
    """
    next_page = request.GET.get('next')
    auth_method = request.session.get('auth-method')

    if not next_page and get_setting('UNIAUTH_LOGOUT_REDIRECT_URL'):
        next_page = get_redirect_url(
            request, get_setting('UNIAUTH_LOGOUT_REDIRECT_URL'))

    # Formally log out user
    auth_logout(request)

    # Determine whether the user logged in through an institution's CAS
    institution = None
    if auth_method and auth_method.startswith("cas-"):
        try:
            institution = Institution.objects.get(slug=auth_method[4:])
        except Institution.DoesNotExist:
            pass

    # If we need to logout an institution's CAS,
    # redirect to that CAS server's logout URL
    if institution and get_setting('UNIAUTH_LOGOUT_CAS_COMPLETELY'):
        redirect_url = urlunparse(
            (get_protocol(request), request.get_host(), next_page
             or reverse('uniauth:logout'), '', '', ''))
        client = CASClient(version=2,
                           service_url=get_service_url(request),
                           server_url=institution.cas_server_url)
        return HttpResponseRedirect(client.get_logout_url(redirect_url))

    # If next page is set, proceed to it
    elif next_page:
        return HttpResponseRedirect(next_page)

    # Otherwise, render the logout view
    else:
        return render(request, 'uniauth/logout.html')
Beispiel #4
0
def _login_success(request, user, next_url, drop_params=[]):
    """
    Determines where to go upon successful authentication:
    Redirects to link tmp account page if user is a temporary
    user, redirects to next_url otherwise.

    Any query parameters whose key exists in drop_params are
    not propogated to the destination URL
    """
    query_params = request.GET.copy()
    jwt_auth = get_setting('UNIAUTH_USE_JWT_AUTH')

    # Drop all blacklisted query parameters
    for key in drop_params:
        if key in query_params:
            del query_params[key]

    # Temporary users should redirect to the link-to-profile page
    if is_tmp_user(user):
        query_params[REDIRECT_FIELD_NAME] = next_url
        return HttpResponseRedirect(reverse('uniauth:link-to-profile') + \
                '?' + urlencode(query_params))

    # All other users should redirect to next_url
    else:
        suffix = ''
        if REDIRECT_FIELD_NAME in query_params:
            del query_params[REDIRECT_FIELD_NAME]
        if len(query_params) > 0:
            suffix = '?' + urlencode(query_params)
        if jwt_auth:
            refresh, access = get_jwt_tokens_for_user(user)
            request.session['jwt-refresh'] = refresh
            request.session['jwt-access'] = access
        return HttpResponseRedirect(next_url + suffix)
Beispiel #5
0
 def _run_test(setting):
     try:
         result = get_setting(setting)
         self.fail("get_setting did not raise an exception when"
                 "asked for non-existent setting: '%s' returned '%s'"
                 % (setting, result))
     except KeyError:
         pass
Beispiel #6
0
 def clean(self):
     """
     Ensure the user does not link more than the
     maximum number of linked emails per user.
     """
     cleaned_data = super(AddLinkedEmailForm, self).clean()
     max_linked_emails = get_setting('UNIAUTH_MAX_LINKED_EMAILS')
     if max_linked_emails > 0 and \
             self.user.profile.linked_emails.count() >= max_linked_emails:
         err_msg = (
             "You can not link more than %d emails to your account." %
             max_linked_emails)
         raise forms.ValidationError(err_msg, code="max_emails")
     return cleaned_data
Beispiel #7
0
    def clean(self):
        """
        Ensures an email can't be linked and verified for multiple
        accounts if UNIAUTH_ALLOW_SHARED_EMAILS is False.

        Also ensures a user does not link more than the maximum
        number of linked emails per user.
        """
        from uniauth.utils import get_setting

        # Check for shared emails if necessary
        if not get_setting('UNIAUTH_ALLOW_SHARED_EMAILS') and \
                LinkedEmail.objects.filter(address=self.address,
                        is_verified=True) and self.is_verified:
            raise ValidationError("This email address has already been " +
                                  "linked to another account.")

        # Ensure a user doesn't link more than the maximum
        max_linked_emails = get_setting('UNIAUTH_MAX_LINKED_EMAILS')
        if max_linked_emails > 0 and \
                self.profile.linked_emails.count() >= max_linked_emails:
            raise ValidationError(("You can not link more than %d emails "
                                   "to your account.") % max_linked_emails)
        super(LinkedEmail, self).clean()
Beispiel #8
0
class PasswordReset(PasswordResetView):
    """
    Asks user for an email address to send the link to, then
    sends a password reset link to that address.

    If no user has that email address linked to their account,
    no email is sent, unbeknownst to the user.
    """
    email_template_name = 'uniauth/password-reset-email.html'
    form_class = PasswordResetForm
    from_email = get_setting('UNIAUTH_FROM_EMAIL')
    success_url = reverse_lazy('uniauth:password-reset-done')
    template_name = 'uniauth/password-reset.html'

    def form_valid(self, form):
        """
        Add global context data before composing reset email
        """
        self.extra_email_context = _get_global_context(self.request)
        return super(PasswordReset, self).form_valid(form)
Beispiel #9
0
    def clean_email(self):
        """
        Ensures that you can't sign up with an email address
        another verified user already has their primary email.

        If UNIAUTH_ALLOW_SHARED_EMAILS is False, ensures the
        email hasn't been linked + verified by any profile.
        """
        email = self.cleaned_data.get('email')
        if get_user_model().objects.filter(email=email).exists():
            err_msg = ("A user with that primary email address already "
                       "exists. Please choose another.")
            raise forms.ValidationError(err_msg, code="email_taken")
        if not get_setting('UNIAUTH_ALLOW_SHARED_EMAILS') and \
                LinkedEmail.objects.filter(address=email,
                        is_verified=True).exists():
            err_msg = ("That email address has already been linked "
                       "to another account.")
            raise forms.ValidationError(err_msg, code="already_linked")
        return email
Beispiel #10
0
    def clean_email(self):
        """
        Ensures you can't link an email that has already
        been linked to the current Uniauth profile.

        If UNIAUTH_ALLOW_SHARED_EMAILS is False, ensures
        the email hasn't been linked to any profile.
        """
        email = self.cleaned_data.get('email')
        if LinkedEmail.objects.filter(profile=self.user.profile,
                                      address=email).exists():
            err_msg = ("That email address has already been linked "
                       "to this account.")
            raise forms.ValidationError(err_msg, code="already_linked")
        if not get_setting('UNIAUTH_ALLOW_SHARED_EMAILS') and \
                LinkedEmail.objects.filter(address=email,
                        is_verified=True).exists():
            err_msg = ("That email address has already been linked "
                       "to another account.")
            raise forms.ValidationError(err_msg, code="already_linked")
        return email
Beispiel #11
0
def _send_verification_email(request, to_email, verify_email):
    """
    Sends an email (to to_email) containing a link to
    verify the target email (verify_email) as a linked
    email address for the user.

    Expects to_email as a string, and verify_email as
    a LinkedEmail model instance.
    """
    subject = "Verify your email address."
    message = render_to_string(
        'uniauth/verification-email.html', {
            'protocol': get_protocol(request),
            'domain': get_current_site(request),
            'pk': encode_pk(verify_email.pk),
            'token': token_generator.make_token(verify_email),
            'query_params': _get_global_context(request)["query_params"],
        })
    email = EmailMessage(subject,
                         message,
                         to=[to_email],
                         from_email=get_setting('UNIAUTH_FROM_EMAIL'))
    email.send()
Beispiel #12
0
def merge_model_instances(primary_object, alias_objects, field_trace=[]):
    """
    Merge several model instances into one, the `primary_object`.
    Use this function to merge model objects and migrate all of the
    related fields from the alias objects into the primary object.

    Performs recursive merging of related One-to-One fields.
    """
    generic_fields = _get_generic_fields()

    # get related fields
    related_fields = list(filter(
        lambda x: x.is_relation is True,
        primary_object._meta.get_fields()))

    many_to_many_fields = list(filter(
        lambda x: x.many_to_many is True, related_fields))

    related_fields = list(filter(
        lambda x: x.many_to_many is False, related_fields))

    # Loop through all alias objects and migrate their references to the
    # primary object
    deleted_objects = []
    deleted_objects_count = 0
    for alias_object in alias_objects:
        # Migrate all foreign key references from alias object to primary
        # object.
        for many_to_many_field in many_to_many_fields:
            alias_varname = many_to_many_field.name
            related_objects = getattr(alias_object, alias_varname)
            for obj in related_objects.all():
                try:
                    # Handle regular M2M relationships.
                    getattr(alias_object, alias_varname).remove(obj)
                    getattr(primary_object, alias_varname).add(obj)
                except AttributeError:
                    # Handle M2M relationships with a 'through' model.
                    # This does not delete the 'through model.
                    through_model = getattr(alias_object, alias_varname).through
                    kwargs = {
                        many_to_many_field.m2m_reverse_field_name(): obj,
                        many_to_many_field.m2m_field_name(): alias_object,
                    }
                    through_model_instances = through_model.objects.filter(**kwargs)
                    for instance in through_model_instances:
                        # Re-attach the through model to the primary_object
                        setattr(
                            instance,
                            many_to_many_field.m2m_field_name(),
                            primary_object)
                        instance.save()

        for related_field in related_fields:
            if related_field.one_to_many:
                alias_varname = related_field.get_accessor_name()
                related_objects = getattr(alias_object, alias_varname)
                for obj in related_objects.all():
                    field_name = related_field.field.name
                    setattr(obj, field_name, primary_object)
                    obj.save()
            elif related_field.one_to_one or related_field.many_to_one:
                alias_varname = related_field.name
                related_object = getattr(alias_object, alias_varname)
                primary_related_object = getattr(primary_object, alias_varname)
                if primary_related_object is None:
                    setattr(primary_object, alias_varname, related_object)
                    primary_object.save()
                elif related_field.one_to_one:
                    # Perform recurisve merging for one-to-one fields
                    if get_setting("UNIAUTH_PERFORM_RECURSIVE_MERGING"):
                        if related_field in field_trace:
                            continue
                        updated_trace = field_trace + [related_field,
                                related_field.remote_field]
                        merge_model_instances(primary_related_object,
                                [related_object], updated_trace)
                    else:
                        related_object.delete()

        for field in generic_fields:
            filter_kwargs = {}
            filter_kwargs[field.fk_field] = alias_object._get_pk_val()
            filter_kwargs[field.ct_field] = field.get_content_type(alias_object)
            related_objects = field.model.objects.filter(**filter_kwargs)
            for generic_related_object in related_objects:
                setattr(generic_related_object, field.name, primary_object)
                generic_related_object.save()

        if alias_object.id:
            deleted_objects += [alias_object]
            alias_object.delete()
            deleted_objects_count += 1

    return primary_object, deleted_objects, deleted_objects_count
Beispiel #13
0
def verify_token(request, pk_base64, token):
    """
    Verifies a token generated for validating an email
    address, and notifies the user whether verification
    was successful.
    """
    next_url = request.GET.get('next') or request.GET.get(REDIRECT_FIELD_NAME)
    context = {'next_url': next_url, 'is_signup': False}
    user_model = get_user_model()

    # Attempt to get the linked email to verify
    try:
        email_pk = decode_pk(pk_base64)
        email = LinkedEmail.objects.get(pk=email_pk)
    except (TypeError, ValueError, OverflowError, user_model.DoesNotExist,
            LinkedEmail.DoesNotExist):
        email = None

    # In the unlikely scenario that a user is trying to sign up
    # with an email another verified user has as a primary email
    # address, reject verification immediately
    if email is not None and is_tmp_user(email.profile.user) and \
            get_user_model().objects.filter(email=email.address).exists():
        email = None

    # If the token successfully verifies, update the linked email
    if email is not None and token_generator.check_token(email, token):
        email.is_verified = True
        email.save()

        # If the user this email is linked to is a temporary
        # one, change it to a fully registered user
        user = email.profile.user
        if is_tmp_user(user) or is_unlinked_account(user):
            context['is_signup'] = True
            old_username = user.username

            # Change the email + username to the verified email
            user.email = email.address
            user.username = choose_username(user.email)
            user.save()

            # If the user was created via CAS, add the institution
            # account described by the temporary username
            if old_username.startswith("cas"):
                username_split = get_account_username_split(old_username)
                _add_institution_account(user.profile, username_split[1],
                                         username_split[2])

        # If UNIAUTH_ALLOW_SHARED_EMAILS is False, and there were
        # pending LinkedEmails for this address on other accounts,
        # delete them
        if not get_setting('UNIAUTH_ALLOW_SHARED_EMAILS'):
            LinkedEmail.objects.filter(address=email.address,
                                       is_verified=False).delete()

        return render(request, 'uniauth/verification-success.html', context)

    # If anything went wrong, just render the failed verification template
    else:
        return render(request, 'uniauth/verification-failure.html', context)
Beispiel #14
0
def login(request):
    """
    Authenticates the user, then redirects them to the
    next page, defaulting to the URL specified by the
    UNIAUTH_LOGIN_REDIRECT_URL setting.

    Offers users the choice between logging in with their
    Uniauth credentials, or through the CAS interface for
    any supported institution.
    """
    next_url = request.GET.get('next')
    context = _get_global_context(request)

    if not next_url:
        next_url = get_redirect_url(request)

    # If the user is already authenticated, proceed to next page
    if request.user.is_authenticated:
        return _login_success(request, request.user, next_url)

    display_standard = get_setting('UNIAUTH_LOGIN_DISPLAY_STANDARD')
    display_cas = get_setting('UNIAUTH_LOGIN_DISPLAY_CAS')
    num_institutions = len(context['institutions'])

    # Ensure the login settings are configured correctly
    if not display_standard and not display_cas:
        err_msg = "At least one of '%s' and '%s' must be True." % \
                ('UNIAUTH_LOGIN_DISPLAY_STANDARD', 'UNIAUTH_LOGIN_DISPLAY_CAS')
        raise ImproperlyConfigured(err_msg)
    if display_cas and num_institutions == 0:
        err_msg = ("'%s' is True, but there are no Institutions in the "
                   "database to log into!") % 'UNIAUTH_LOGIN_DISPLAY_CAS'
        raise ImproperlyConfigured(err_msg)

    context['display_standard'] = display_standard
    context['display_cas'] = display_cas
    context['num_institutions'] = num_institutions

    # If we aren't displaying the standard login form,
    # we're just displaying the CAS login options
    if not display_standard:
        institutions = context['institutions']
        query_params = context['query_params']

        # If there's only one possible institution to log
        # into, redirect to its CAS login page immediately
        if num_institutions == 1:
            return HttpResponseRedirect(institutions[0][2] + query_params)

        # Otherwise, render the page (without the Login form)
        else:
            return render(request, 'uniauth/login.html', context)

    # If we are displaying the login form, and it's
    # a POST request, attempt to validate the form
    elif request.method == "POST":
        form = LoginForm(request, request.POST)

        # Authentication successful: setup session + proceed
        if form.is_valid():
            user = form.get_user()
            auth_login(request, user)
            request.session['auth-method'] = "uniauth"
            return _login_success(request, user, next_url)

        # Authentication failed: render form errors
        else:
            context['form'] = form
            return render(request, 'uniauth/login.html', context)

    # Otherwise, render a blank Login form
    else:
        form = LoginForm(request)
        context['form'] = form
        return render(request, 'uniauth/login.html', context)
Beispiel #15
0
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
from uniauth import views
from uniauth.utils import get_setting

# The standard login form requires URLs that don't exist when
# cas_only is used. Ensure that this setting is False.
if get_setting('UNIAUTH_LOGIN_DISPLAY_STANDARD'):
    err_msg = ("'uniauth.urls.cas_only' can not be used when %s is True. "
            "Please include 'uniauth.urls' instead.") % \
            'UNIAUTH_LOGIN_DISPLAY_STANDARD'
    raise ImproperlyConfigured(err_msg)

app_name = 'uniauth'

urlpatterns = [
    url(r'^login/$', views.login, name='login'),
    url(r'^cas-login/(?P<institution>[a-z0-9\-]+)/$',
        views.cas_login,
        name='cas-login'),
    url(r'^logout/$', views.logout, name='logout'),
]