Beispiel #1
0
 def test_existing_user_invitation_accepted(self):
     """
     SsoBackend should create a new user if the username passed to does
     not exist and the email domain matches an AuthenticatedEmailDomain
     for the given IdentityProvider. It should also ensure that any
     user data from a registration form and/or the samlUserdata are all
     properly saved to the User model.
     """
     admin_role = StaticRole.domain_admin(domain=self.domain.name)
     existing_user = WebUser.create(None, '*****@*****.**', 'testpwd',
                                    None, None)
     invitation = Invitation(
         domain=self.domain.name,
         email=existing_user.username,
         invited_by=self.user.couch_id,
         invited_on=datetime.datetime.utcnow(),
         role=admin_role.get_qualified_id(),
     )
     invitation.save()
     AsyncSignupRequest.create_from_invitation(invitation)
     user = auth.authenticate(
         request=self.request,
         username=invitation.email,
         idp_slug=self.idp.slug,
         is_handshake_successful=True,
     )
     self.assertIsNotNone(user)
     self.assertEqual(user.username, invitation.email)
     self.assertEqual(self.request.sso_new_user_messages['success'], [
         f'You have been added to the "{invitation.domain}" project space.',
     ])
Beispiel #2
0
 def test_new_user_created_and_invitation_accepted(self):
     """
     When SsoBackend creates a new user and an invitation is present, that
     invitation should add the user to the invited project
     space and accept the invitation
     """
     admin_role = StaticRole.domain_admin(self.domain.name)
     invitation = Invitation(
         domain=self.domain.name,
         email='*****@*****.**',
         invited_by=self.user.couch_id,
         invited_on=datetime.datetime.utcnow(),
         role=admin_role.get_qualified_id(),
     )
     invitation.save()
     AsyncSignupRequest.create_from_invitation(invitation)
     generator.store_full_name_in_saml_user_data(self.request, 'Isa',
                                                 'Baas')
     user = auth.authenticate(
         request=self.request,
         username=invitation.email,
         idp_slug=self.idp.slug,
         is_handshake_successful=True,
     )
     self.assertIsNotNone(user)
     self.assertEqual(user.username, invitation.email)
     self.assertEqual(user.first_name, 'Isa')
     self.assertEqual(user.last_name, 'Baas')
     self.assertEqual(self.request.sso_new_user_messages['success'], [
         f'User account for {invitation.email} created.',
         f'You have been added to the "{invitation.domain}" project space.',
     ])
Beispiel #3
0
 def test_new_user_created_and_expired_invitation_declined(self):
     """
     When SsoBackend creates a new user and an EXPIRED invitation is present,
     a new user should still be created, but the invitation should be declined.
     """
     invitation = Invitation(
         domain=self.domain.name,
         email='*****@*****.**',
         invited_by=self.user.couch_id,
         invited_on=datetime.datetime.utcnow() - relativedelta(months=2),
     )
     invitation.save()
     AsyncSignupRequest.create_from_invitation(invitation)
     generator.store_full_name_in_saml_user_data(self.request, 'Zee', 'Bos')
     user = auth.authenticate(
         request=self.request,
         username=invitation.email,
         idp_slug=self.idp.slug,
         is_handshake_successful=True,
     )
     self.assertIsNotNone(user)
     self.assertEqual(user.username, invitation.email)
     self.assertEqual(user.first_name, 'Zee')
     self.assertEqual(user.last_name, 'Bos')
     self.assertEqual(self.request.sso_new_user_messages['success'], [
         f'User account for {invitation.email} created.',
     ])
     self.assertEqual(self.request.sso_new_user_messages['error'], [
         'Could not accept invitation because it is expired.',
     ])
Beispiel #4
0
 def test_new_user_displayname_is_used_if_first_and_last_are_missing(self):
     """
     Azure AD does not mark the First and Last names as required, only the
     Display Name. If First and Last are missing, ensure that this
     information is then obtained from the Display Name
     """
     username = '******'
     reg_form = RegisterWebUserForm()
     reg_form.cleaned_data = {
         'email': username,
         'phone_number': '+15555555555',
         'project_name': 'test-vault',
         'persona': 'Other',
         'persona_other': "for tests",
     }
     generator.store_display_name_in_saml_user_data(self.request,
                                                    'Vanessa van Beek')
     AsyncSignupRequest.create_from_registration_form(reg_form)
     user = auth.authenticate(
         request=self.request,
         username=username,
         idp_slug=self.idp.slug,
         is_handshake_successful=True,
     )
     self.assertIsNotNone(user)
     self.assertEqual(user.username, username)
     self.assertEqual(user.first_name, 'Vanessa')
     self.assertEqual(user.last_name, 'van Beek')
     self.assertEqual(self.request.sso_new_user_messages['success'],
                      ["User account for [email protected] created."])
Beispiel #5
0
 def test_new_user_displayname_with_one_name_is_used_as_first_name(self):
     """
     Ensure that if the Azure AD "Display Name" has only one name/word in
     it that only the first name is populated.
     """
     username = '******'
     reg_form = RegisterWebUserForm()
     reg_form.cleaned_data = {
         'email': username,
         'phone_number': '+15555555555',
         'project_name': 'test-vault',
         'persona': 'Other',
         'persona_other': "for tests",
     }
     generator.store_display_name_in_saml_user_data(self.request, 'Test')
     AsyncSignupRequest.create_from_registration_form(reg_form)
     user = auth.authenticate(
         request=self.request,
         username=username,
         idp_slug=self.idp.slug,
         is_handshake_successful=True,
     )
     self.assertIsNotNone(user)
     self.assertEqual(user.username, username)
     self.assertEqual(user.first_name, 'Test')
     self.assertEqual(user.last_name, '')
     self.assertEqual(self.request.sso_new_user_messages['success'],
                      ["User account for [email protected] created."])
Beispiel #6
0
 def test_new_user_created_and_data_is_saved(self):
     """
     SsoBackend should create a new user if the username passed to does
     not exist and the email domain matches an AuthenticatedEmailDomain
     for the given IdentityProvider. It should also ensure that any
     user data from a registration form and/or the samlUserdata are all
     properly saved to the User model.
     """
     username = '******'
     reg_form = RegisterWebUserForm()
     reg_form.cleaned_data = {
         'email': username,
         'phone_number': '+15555555555',
         'project_name': 'test-vault',
         'persona': 'Other',
         'persona_other': "for tests",
     }
     generator.store_full_name_in_saml_user_data(self.request, 'Maarten',
                                                 'van der Berg')
     AsyncSignupRequest.create_from_registration_form(reg_form)
     user = auth.authenticate(
         request=self.request,
         username=username,
         idp_slug=self.idp.slug,
         is_handshake_successful=True,
     )
     self.assertIsNotNone(user)
     self.assertEqual(user.username, username)
     self.assertEqual(user.first_name, 'Maarten')
     self.assertEqual(user.last_name, 'van der Berg')
     web_user = WebUser.get_by_username(user.username)
     self.assertEqual(web_user.phone_numbers[0], '+15555555555')
     self.assertEqual(self.request.sso_new_user_messages['success'],
                      ["User account for [email protected] created."])
Beispiel #7
0
    def register_new_user(self, data):
        idp = None
        if settings.ENFORCE_SSO_LOGIN:
            idp = IdentityProvider.get_required_identity_provider(
                data['data']['email'])

        reg_form = RegisterWebUserForm(data['data'], is_sso=idp is not None)
        if reg_form.is_valid():
            ab_test = ab_tests.SessionAbTest(ab_tests.APPCUES_V3_APP,
                                             self.request)
            appcues_ab_test = ab_test.context['version']

            if idp:
                signup_request = AsyncSignupRequest.create_from_registration_form(
                    reg_form,
                    additional_hubspot_data={
                        "appcues_test": appcues_ab_test,
                    })
                return {
                    'success': True,
                    'appcues_ab_test': appcues_ab_test,
                    'ssoLoginUrl': idp.get_login_url(signup_request.username),
                    'ssoIdpName': idp.name,
                }

            self._create_new_account(reg_form,
                                     additional_hubspot_data={
                                         "appcues_test": appcues_ab_test,
                                     })
            try:
                request_new_domain(self.request,
                                   reg_form.cleaned_data['project_name'],
                                   is_new_user=True)
            except NameUnavailableException:
                # technically, the form should never reach this as names are
                # auto-generated now. But, just in case...
                logging.error(
                    "There as an issue generating a unique domain name "
                    "for a user during new registration.")
                return {
                    'errors': {
                        'project name unavailable': [],
                    }
                }
            return {
                'success': True,
                'appcues_ab_test': appcues_ab_test,
            }
        logging.error(
            "There was an error processing a new user registration form."
            "This shouldn't happen as validation should be top-notch "
            "client-side. Here is what the errors are: {}".format(
                reg_form.errors))
        return {
            'errors': reg_form.errors,
        }
Beispiel #8
0
    def authenticate(self, request, username, idp_slug, is_handshake_successful):
        if not (request and username and idp_slug and is_handshake_successful):
            return None

        username = username.lower()

        try:
            identity_provider = IdentityProvider.objects.get(slug=idp_slug)
        except IdentityProvider.DoesNotExist:
            # not sure how we would even get here, but just in case
            request.sso_login_error = f"Identity Provider {idp_slug} does not exist."
            return None

        if not identity_provider.is_active:
            request.sso_login_error = f"This Identity Provider {idp_slug} is not active."
            return None

        email_domain = get_email_domain_from_username(username)
        if not email_domain:
            # not a valid username
            request.sso_login_error = f"Username {username} is not valid."
            return None

        if not AuthenticatedEmailDomain.objects.filter(
            email_domain=email_domain, identity_provider=identity_provider
        ).exists():
            # if this user's email domain is not authorized by this identity
            # do not continue with authentication
            request.sso_login_error = (
                f"The Email Domain {email_domain} is not allowed to "
                f"authenticate with this Identity Provider ({idp_slug})."
            )
            return None

        async_signup = AsyncSignupRequest.get_by_username(username)

        # because the django messages middleware is not yet available...
        request.sso_new_user_messages = {
            'success': [],
            'error': [],
        }

        try:
            user = User.objects.get(username=username)
            is_new_user = False
            web_user = WebUser.get_by_username(username)
        except User.DoesNotExist:
            user, web_user = self._create_new_user(request, username, async_signup)
            is_new_user = True

        if async_signup and async_signup.invitation:
            self._process_invitation(request, async_signup.invitation, web_user, is_new_user)

        request.sso_login_error = None
        return user
Beispiel #9
0
def sso_test_create_user(request, idp_slug):
    """
    A testing view exclusively for staging. This will be removed once the
    UIs are in place to sign up users or invite new users who must log in with
    SSO.
    """
    if settings.SERVER_ENVIRONMENT not in ['staging']:
        raise Http404()

    username = request.GET.get('username')
    if username:
        prepare_session_with_sso_username(request, username)

    invitation_uuid = request.GET.get('invitation')
    invitation = Invitation.objects.get(
        uuid=invitation_uuid) if invitation_uuid else None
    if invitation:
        AsyncSignupRequest.create_from_invitation(invitation)

    return HttpResponseRedirect(reverse("sso_saml_login", args=(idp_slug, )))
Beispiel #10
0
def sso_saml_acs(request, idp_slug):
    """
    ACS stands for "Assertion Consumer Service". The Identity Provider will send
    its response to this view after authenticating a user. This is often
    referred to as the "Entity ID" in the IdP's Service Provider configuration.

    In this view we verify the received SAML 2.0 response and then log in the user
    to CommCare HQ.
    """
    request_id = request.session.get('AuthNRequestID')
    error_template = 'sso/acs_errors.html'

    try:
        request.saml2_auth.process_response(request_id=request_id)
        errors = request.saml2_auth.get_errors()
    except OneLogin_Saml2_Error as e:
        if e.code == OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND:
            return redirect("sso_saml_login", idp_slug=idp_slug)
        errors = [e]

    if errors:
        return render(
            request,
            error_template,
            {
                'saml_error_reason':
                request.saml2_auth.get_last_error_reason() or errors[0],
                'idp_type':
                "Azure AD",  # we will update this later,
                'docs_link':
                get_documentation_url(request.idp),
            })

    if not request.saml2_auth.is_authenticated():
        return render(request, 'sso/sso_request_denied.html', {})

    if 'AuthNRequestID' in request.session:
        del request.session['AuthNRequestID']

    store_saml_data_in_session(request)

    user = auth.authenticate(
        request=request,
        username=request.session['samlNameId'],
        idp_slug=idp_slug,
        is_handshake_successful=True,
    )

    # we add the messages to the django messages framework here since
    # that middleware was not available for SsoBackend
    if hasattr(request, 'sso_new_user_messages'):
        for success_message in request.sso_new_user_messages['success']:
            messages.success(request, success_message)
        for error_message in request.sso_new_user_messages['error']:
            messages.error(request, error_message)

    if user:
        auth.login(request, user)

        # activate new project if needed
        async_signup = AsyncSignupRequest.get_by_username(user.username)
        if async_signup and async_signup.project_name:
            try:
                request_new_domain(request,
                                   async_signup.project_name,
                                   is_new_user=True,
                                   is_new_sso_user=True)
            except NameUnavailableException:
                # this should never happen, but in the off chance it does
                # we don't want to throw a 500 on this view
                messages.error(
                    request,
                    _("We were unable to create your requested project "
                      "because the name was already taken."
                      "Please contact support."))

        AsyncSignupRequest.clear_data_for_username(user.username)

        relay_state = request.saml2_request_data['post_data'].get('RelayState')
        if relay_state not in [
                OneLogin_Saml2_Utils.get_self_url(request.saml2_request_data),
                get_saml_login_url(request.idp),
        ]:
            # redirect to next=<relay_state>
            return HttpResponseRedirect(
                request.saml2_auth.redirect_to(relay_state))

        return redirect("homepage")

    return render(request, error_template, {
        'login_error': getattr(request, 'sso_login_error', None),
    })
Beispiel #11
0
    def __call__(self, request, uuid, **kwargs):
        # add the correct parameters to this instance
        self.request = request
        if 'domain' in kwargs:
            self.domain = kwargs['domain']

        if request.GET.get('switch') == 'true':
            logout(request)
            return redirect_to_login(request.path)
        if request.GET.get('create') == 'true':
            logout(request)
            return HttpResponseRedirect(request.path)
        try:
            invitation = Invitation.objects.get(uuid=uuid)
        except (Invitation.DoesNotExist, ValidationError):
            messages.error(
                request,
                _("Sorry, it looks like your invitation has expired. "
                  "Please check the invitation link you received and try again, or "
                  "request a project administrator to send you the invitation again."
                  ))
            return HttpResponseRedirect(reverse("login"))

        if invitation.is_accepted:
            messages.error(
                request,
                _("Sorry, that invitation has already been used up. "
                  "If you feel this is a mistake please ask the inviter for "
                  "another invitation."))
            return HttpResponseRedirect(reverse("login"))

        self.validate_invitation(invitation)

        if invitation.is_expired:
            return HttpResponseRedirect(reverse("no_permissions"))

        # Add zero-width space to username for better line breaking
        username = self.request.user.username.replace("@", "&#x200b;@")
        context = {
            'formatted_username': username,
            'domain': self.domain,
            'invite_to': self.domain,
            'invite_type': _('Project'),
            'hide_password_feedback': has_custom_clean_password(),
        }
        if request.user.is_authenticated:
            context['current_page'] = {'page_name': _('Project Invitation')}
        else:
            context['current_page'] = {
                'page_name': _('Project Invitation, Account Required')
            }
        if request.user.is_authenticated:
            is_invited_user = request.couch_user.username.lower(
            ) == invitation.email.lower()
            if self.is_invited(invitation, request.couch_user
                               ) and not request.couch_user.is_superuser:
                if is_invited_user:
                    # if this invite was actually for this user, just mark it accepted
                    messages.info(
                        request,
                        _("You are already a member of {entity}.").format(
                            entity=self.inviting_entity))
                    invitation.is_accepted = True
                    invitation.save()
                else:
                    messages.error(
                        request,
                        _("It looks like you are trying to accept an invitation for "
                          "{invited} but you are already a member of {entity} with the "
                          "account {current}. Please sign out to accept this invitation "
                          "as another user.").format(
                              entity=self.inviting_entity,
                              invited=invitation.email,
                              current=request.couch_user.username))
                return HttpResponseRedirect(
                    self.redirect_to_on_success(invitation.email, self.domain))

            if not is_invited_user:
                messages.error(
                    request,
                    _("The invited user {invited} and your user {current} "
                      "do not match!").format(
                          invited=invitation.email,
                          current=request.couch_user.username))

            if request.method == "POST":
                couch_user = CouchUser.from_django_user(request.user,
                                                        strict=True)
                invitation.accept_invitation_and_join_domain(couch_user)
                log_user_change(
                    by_domain=invitation.domain,
                    for_domain=invitation.domain,
                    couch_user=couch_user,
                    changed_by_user=CouchUser.get_by_user_id(
                        invitation.invited_by),
                    changed_via=USER_CHANGE_VIA_INVITATION,
                    change_messages=UserChangeMessage.domain_addition(
                        invitation.domain))
                track_workflow(
                    request.couch_user.get_email(),
                    "Current user accepted a project invitation",
                    {"Current user accepted a project invitation": "yes"})
                send_hubspot_form(HUBSPOT_EXISTING_USER_INVITE_FORM, request)
                return HttpResponseRedirect(
                    self.redirect_to_on_success(invitation.email, self.domain))
            else:
                mobile_user = CouchUser.from_django_user(
                    request.user).is_commcare_user()
                context.update({
                    'mobile_user':
                    mobile_user,
                    "invited_user":
                    invitation.email
                    if request.couch_user.username != invitation.email else "",
                })
                return render(request, self.template, context)
        else:
            idp = None
            if settings.ENFORCE_SSO_LOGIN:
                idp = IdentityProvider.get_active_identity_provider_by_username(
                    invitation.email)

            if request.method == "POST":
                form = WebUserInvitationForm(request.POST,
                                             is_sso=idp is not None)
                if form.is_valid():
                    # create the new user
                    invited_by_user = CouchUser.get_by_user_id(
                        invitation.invited_by)

                    if idp:
                        signup_request = AsyncSignupRequest.create_from_invitation(
                            invitation)
                        return HttpResponseRedirect(
                            idp.get_login_url(signup_request.username))

                    user = activate_new_user_via_reg_form(
                        form,
                        created_by=invited_by_user,
                        created_via=USER_CHANGE_VIA_INVITATION,
                        domain=invitation.domain,
                        is_domain_admin=False,
                    )
                    user.save()
                    messages.success(
                        request,
                        _("User account for %s created!") %
                        form.cleaned_data["email"])
                    invitation.accept_invitation_and_join_domain(user)
                    messages.success(
                        self.request,
                        _('You have been added to the "{}" project space.').
                        format(self.domain))
                    authenticated = authenticate(
                        username=form.cleaned_data["email"],
                        password=form.cleaned_data["password"],
                        request=request)
                    if authenticated is not None and authenticated.is_active:
                        login(request, authenticated)
                    track_workflow(
                        request.POST['email'],
                        "New User Accepted a project invitation",
                        {"New User Accepted a project invitation": "yes"})
                    send_hubspot_form(HUBSPOT_NEW_USER_INVITE_FORM, request,
                                      user)
                    return HttpResponseRedirect(
                        self.redirect_to_on_success(invitation.email,
                                                    invitation.domain))
            else:
                if (CouchUser.get_by_username(invitation.email)
                        or User.objects.filter(
                            username__iexact=invitation.email).count() > 0):
                    login_url = reverse("login")
                    accept_invitation_url = reverse(
                        'domain_accept_invitation',
                        args=[invitation.domain, invitation.uuid])
                    return HttpResponseRedirect(
                        f"{login_url}"
                        f"?next={accept_invitation_url}"
                        f"&username={invitation.email}")
                form = WebUserInvitationForm(
                    initial={
                        'email': invitation.email,
                    },
                    is_sso=idp is not None,
                )

            context.update({
                'is_sso': idp is not None,
                'idp_name': idp.name if idp else None,
                'invited_user': invitation.email,
            })

        context.update({"form": form})
        return render(request, self.template, context)
Beispiel #12
0
def sso_saml_acs(request, idp_slug):
    """
    ACS stands for "Assertion Consumer Service". The Identity Provider will send
    its response to this view after authenticating a user. This is often
    referred to as the "Entity ID" in the IdP's Service Provider configuration.

    In this view we verify the received SAML 2.0 response and then log in the user
    to CommCare HQ.
    """
    # todo these are placeholders for the json dump below
    error_reason = None
    request_session_data = None
    saml_relay = None

    request_id = request.session.get('AuthNRequestID')
    processed_response = request.saml2_auth.process_response(
        request_id=request_id)
    errors = request.saml2_auth.get_errors()
    not_auth_warn = not request.saml2_auth.is_authenticated()

    if not errors:
        if 'AuthNRequestID' in request.session:
            del request.session['AuthNRequestID']

        store_saml_data_in_session(request)

        user = auth.authenticate(
            request=request,
            username=request.session['samlNameId'],
            idp_slug=idp_slug,
            is_handshake_successful=True,
        )

        # we add the messages to the django messages framework here since
        # that middleware was not available for SsoBackend
        if hasattr(request, 'sso_new_user_messages'):
            for success_message in request.sso_new_user_messages['success']:
                messages.success(request, success_message)
            for error_message in request.sso_new_user_messages['error']:
                messages.error(request, error_message)

        if user:
            auth.login(request, user)

            # activate new project if needed
            async_signup = AsyncSignupRequest.get_by_username(user.username)
            if async_signup and async_signup.project_name:
                try:
                    request_new_domain(request,
                                       async_signup.project_name,
                                       is_new_user=True)
                except NameUnavailableException:
                    # this should never happen, but in the off chance it does
                    # we don't want to throw a 500 on this view
                    messages.error(
                        request,
                        _("We were unable to create your requested project "
                          "because the name was already taken."
                          "Please contact support."))

            AsyncSignupRequest.clear_data_for_username(user.username)
            return redirect("homepage")

        # todo for debugging purposes to dump into the response below
        request_session_data = {
            "samlUserdata":
            request.session['samlUserdata'],
            "samlNameId":
            request.session['samlNameId'],
            "samlNameIdFormat":
            request.session['samlNameIdFormat'],
            "samlNameIdNameQualifier":
            request.session['samlNameIdNameQualifier'],
            "samlNameIdSPNameQualifier":
            request.session['samlNameIdSPNameQualifier'],
            "samlSessionIndex":
            request.session['samlSessionIndex'],
        }

    else:
        error_reason = request.saml2_auth.get_last_error_reason()

    return HttpResponse(
        json.dumps({
            "errors": errors,
            "error_reason": error_reason,
            "not_auth_warn": not_auth_warn,
            "request_id": request_id,
            "processed_response": processed_response,
            "saml_relay": saml_relay,
            "request_session_data": request_session_data,
            "login_error": getattr(request, 'sso_login_error', None),
        }), 'text/json')