class AssociateHandler(ApiHandler):
    """Action is either 'associate' or 'set_owner'."""
    def do(self, action, from_kind, from_id, to_kind, to_id):
        from_klass = core.kind_to_class(from_kind)
        to_klass = core.kind_to_class(to_kind)
        from_entity = from_klass.get_by_id(from_id)
        to_entity = to_klass.get_by_id(to_id)
        from_entity = self.api.associate(action, from_entity, to_entity)

        # We'll want to return this at the end.
        data = from_entity.to_dict()

        # Special case for activity management: when teachers associate with a
        # cohort or classroom for the first time, activity entities need to be
        # created for them.

        init_activities = (
            from_kind == 'user' and from_entity.user_type == 'teacher'
            and ((to_kind == 'cohort' and action == 'associate') or
                 (to_kind == 'classroom' and action == 'set_owner')))
        if init_activities:
            # To simulate a fresh call, refresh the user in the Api object.
            # This only applies when a user is associating *themselves* with a
            # cohort or classroom. Without this refresh, the new associations
            # created just above won't be there and permissions to associate
            # the new activities will be denied.
            if (self.api.user.id == from_id):
                self.api = Api(from_entity)

            # If the classroom or cohort being associated to is a testing
            # entity, then these activities should also be.
            kwargs = {'is_test': to_entity.is_test}
            program_id = to_entity.assc_program_list[0]
            if to_kind == 'cohort':
                kwargs['cohort_id'] = to_entity.id
                user_type = 'teacher'
            if to_kind == 'classroom':
                kwargs['cohort_id'] = to_entity.assc_cohort_list[0]
                kwargs['classroom_id'] = to_entity.id
                user_type = 'student'
            teacher_id = from_entity.id
            activities = self.api.init_activities(user_type, teacher_id,
                                                  program_id, **kwargs)

            # If these activities are being created FOR the teacher by an admin
            # or researcher, we need to do extra work to make sure those
            # activities are owned by the teacher.
            if self.get_current_user() != from_entity:
                for a in activities:
                    self.api.associate('set_owner', from_entity, a)

            # Include the created activities with the modified entity so the
            # client gets them immediately. This allows client views to update
            # immediately if necessary.
            data['_teacher_activity_list'] = [a.to_dict() for a in activities]

        return {'success': True, 'data': data}
class PertsTestCase(unittest.TestCase):
    """Contains important global settings for running unit tests.

    Errors related to logging, and appstats not being able to access memcache,
    would appear without these settings.

    Use Example:
    ```
    class MyTestCase(unit_test_help.PertsTestCase):
        def set_up(self):
            # Let PertsTestCase do its important work
            super(MyTestCase, self).setUp()

            # Add your own stubs here
            self.testbed.init_user_stub()

        # Add your tests here
        def test_my_stuff(self):
            pass
    ```
    """
    def setUp(self):
        """Sets self.testbed and activates it, among other global settings.

        This function (noticeably not named with PERTS-standard undrescore
        case) is automatically called when starting a test by the unittest
        module. We use it for basic configuration and delegate further set
        up to the more canonically named set_up() of inheriting classes.
        """
        if not util.is_localhost():
            # Logging test activity in production causes errors. This
            # suppresses all logs of level critical and lower, which means all
            # of them. See
            # https://docs.python.org/2/library/logging.html#logging.disable
            logging.disable(logging.CRITICAL)

        # Start a clean testing environment for one test.
        self.testbed = testbed.Testbed()
        self.testbed.activate()

        # user services
        self.testbed.init_user_stub()

        # Writing students involves tasks and memcache, so we need stubs for
        # those.
        self.testbed.init_memcache_stub()
        self.testbed.init_taskqueue_stub(root_path='.')
        self.taskqueue_stub = self.testbed.get_stub(
            testbed.TASKQUEUE_SERVICE_NAME)

        # Basic apis not related to specific users.
        self.public_api = Api(User(user_type='public'))
        self.internal_api = Api(User(user_type='god'))

        # Let inheriting classes to their own set up.
        if hasattr(self, 'set_up'):
            self.set_up()

    def tearDown(self):
        """Automatically called at end of test by the unittest module."""
        # Re-enable logging.
        logging.disable(logging.NOTSET)
        # Tear down the testing environment used by a single test so the next
        # test gets a fresh start.
        self.testbed.deactivate()

    def populate(self):
        """Creates a standard set of entities as PERTS expects them to exist.

        Includes all the correct relationships, and leaves everything in a
        consistent state. Mimics the javascript populate script in god.html.
        """
        self.researcher = User.create(user_type='researcher')
        self.researcher_api = Api(self.researcher)
        self.researcher.put()

        self.school = self.researcher_api.create('school', {'name': 'DGN'})
        self.program = self.researcher_api.create('program', {
            'name': 'Test Program',
            'abbreviation': 'TP1',
        })
        self.cohort = self.researcher_api.create(
            'cohort', {
                'name': 'DGN 2014',
                'code': 'trout viper',
                'program': self.program.id,
                'school': self.school.id,
            })

        self.school_admin = self.internal_api.create(
            'user', {'user_type': 'school_admin'})
        self.school_admin_api = Api(self.school_admin)
        # have the researcher set the school_admin as an owner of their cohort
        self.researcher_api.associate('set_owner', self.school_admin,
                                      self.cohort)

        self.teacher = self.public_api.create('user', {'user_type': 'teacher'})
        self.teacher.put()
        self.teacher_api = Api(self.teacher)
        self.researcher_api.associate('associate', self.teacher, self.cohort)

        # This is normally done api_handlers.AssociateHandler
        self.teacher_activities = self.teacher_api.init_activities(
            'teacher',
            self.teacher.id,
            self.program.id,
            cohort_id=self.cohort.id)

        # Different in Yosemite: school_admins create classrooms.
        self.classroom = self.school_admin_api.create(
            'classroom', {
                'name': "English 101",
                'user': self.teacher.id,
                'program': self.program.id,
                'cohort': self.cohort.id,
            })

        self.student = self.public_api.create('user', {
            'user_type': 'student',
            'classroom': self.classroom.id,
        })
        self.student.put()
        self.student_api = Api(self.student)

        # This is normally done api_handlers.CreateHandler
        # Different in Yosemite: school_admins create classrooms (and thus
        # student activities).
        self.student_activities = self.school_admin_api.init_activities(
            'student',
            self.teacher.id,
            self.program.id,
            cohort_id=self.cohort.id,
            classroom_id=self.classroom.id)

        # Force everything into a consistent state, just in case we're using
        # an inconsistent policy.
        db.get([
            self.researcher.key(),
            self.school_admin.key(),
            self.teacher.key(),
            self.student.key(),
            self.school.key(),
            self.program.key(),
            self.cohort.key(),
            self.classroom.key(), self.teacher_activities[0].key(),
            self.teacher_activities[1].key(), self.student_activities[0].key(),
            self.student_activities[1].key()
        ])
Example #3
0
class ProgramAppTestCase(unit_test_helper.PertsTestCase):
    def set_up(self):
        # Load our page handler webapp and wrap it with WebTest's TestApp.
        self.testapp = webtest.TestApp(page_handlers.app)

        # db with well-behaved strong consistency
        self.testbed.init_datastore_v3_stub()

        self.init_context()

    def init_context(self):
        # public api
        self.p_api = Api(User(user_type='public'))

        # This closely follows the pattern of the populate script in god.html
        self.researcher = User(user_type='researcher')
        self.researcher.put()
        self.r_api = Api(self.researcher)
        self.teacher = User.create(user_type='teacher')
        self.teacher.put()
        self.t_api = Api(self.teacher)

        self.school = self.r_api.create('school', {'name': 'DGN'})
        self.program = self.r_api.create('program', {
            'name': 'Test Program',
            'abbreviation': 'TP1',
        })
        self.bad_program = self.r_api.create('program', {
            'name': 'Bad Program',
            'abbreviation': 'TP2',
        })
        self.cohort = self.r_api.create(
            'cohort', {
                'name': 'DGN 2014',
                'code': 'trout viper',
                'program': self.program.id,
                'school': self.school.id,
            })
        self.bad_cohort = self.r_api.create(
            'cohort', {
                'name': 'bad cohort',
                'code': 'king dolphin',
                'program': self.bad_program.id,
                'school': self.school.id,
            })
        self.r_api.associate('associate', self.teacher, self.cohort)
        self.classroom = self.t_api.create(
            'classroom', {
                'name': "English 101",
                'user': self.teacher.id,
                'program': self.program.id,
                'cohort': self.cohort.id,
            })
        # the researcher has to create this one, since the teacher is only
        # associated with the "good" program.
        self.bad_classroom = self.r_api.create(
            'classroom', {
                'name': "English 101",
                'user': self.teacher.id,
                'program': self.bad_program.id,
                'cohort': self.bad_cohort.id,
            })

        # Identify a student into a classroom. See
        # url_handlers._create_student()
        self.student = self.p_api.create('user', {
            'user_type': 'student',
            'classroom': self.classroom.id,
        })
        self.student.put()

    @unittest.skip("Throwing errors in production. Issue #260.")
    def test_malformed_urls(self):
        correct_urls = [
            '/p/TP1/teacher?cohort={}'.format(self.cohort.id),
            '/p/TP1/student?cohort={}&classroom={}'.format(
                self.cohort.id, self.classroom.id),
        ]

        for url in correct_urls:
            response = self.testapp.get(url)
            self.assertEqual(response.status_int, 200, msg=url)

        # Now we test a bunch of urls that return 404s. 404s in webtest raises
        # an exception, which we can assert.

        malformed_urls = [
            # urls with no arguments shouldn't work
            '/p/TP1/student',
            '/p/TP1/teacher',
            # student missing classroom
            '/p/TP1/student?cohort={}'.format(self.cohort.id),
            # student missing cohort
            '/p/TP1/student?classroom={}'.format(self.classroom.id),
            # teacher, mis-associated cohort
            '/p/TP1/teacher?cohort={}'.format(self.bad_cohort.id),
            # student, mis-associated cohort
            '/p/TP1/teacher?cohort={}&classroom={}'.format(
                self.bad_cohort.id, self.classroom.id),
            # student, mis-associated classroom
            '/p/TP1/student?cohort={}&classroom={}'.format(
                self.cohort.id, self.bad_classroom.id),
            # teacher, non-existent cohort
            '/p/TP1/teacher?cohort=DNE',
            # student, non-existent cohort
            '/p/TP1/student?cohort={}&classroom={}'.format(
                'DNE', self.classroom.id),
            # student, non-existent classroom
            '/p/TP1/student?cohort={}&classroom={}'.format(
                self.cohort.id, 'DNE'),
            # student, both non-existent
            '/p/TP1/student?cohort={}&classroom={}'.format('DNE', 'DNE'),
        ]

        for url in malformed_urls:
            self.assertRaises(webtest.AppError, self.testapp.get, url)
Example #4
0
class BaseHandler(webapp2.RequestHandler):
    """Ancestor of all web handlers. Handles sessions and third-party
    authentication."""

    # It is critical to reset these on each request *within a function*, and
    # not just in the class definition. Not all of a given python file is run
    # with every page request. See
    # https://developers.google.com/appengine/docs/python/#imports_are_cached
    # get_current_user() does this work for us
    _user = None
    _impersonated_user = None

    def get(self, *args, **kwargs):
        """Handles ALL requests. Figures out the user and then delegates to
        more purpose-specific methods."""
        # Some poorly-behaved libraries screw with the default logging level,
        # killing our 'info' and 'warning' logs. Make sure it's set correctly
        # for our code.
        util.allow_all_logging()

        # Mark the start time of the request. You can add events to this
        # profiler in any request handler like this:
        # util.profiler.add_event("this is my event")
        util.profiler.clear()
        util.profiler.add_event('START')

        # create and sign in as a god user under certain conditions
        self.immaculately_conceive()
        # Running this ensures that cached users (e.g. self._user) are
        # correctly synched with the session ids.
        self.clean_up_users('user')
        self.clean_up_users('impersonated_user')
        # Deal with impersonation options
        if self.request.get('escape_impersonation') in config.true_strings:
            # Allow calls to escape impersonation, i.e. treat the call as if
            # the true user (not the impersonated user) was making it.
            # Should have no effect when no impersonation is happening.
            self._impersonated_user = None
            self.api = Api(self.get_current_user(method='normal'))
        elif self.request.get('impersonate'):
            # If allowed, impersonate the provided user for just this call
            target_user = User.get_by_id(self.request.get('impersonate'))
            normal_user = self.get_current_user(method='normal')
            if normal_user.can_impersonate(target_user):
                self._impersonated_user = target_user
                self.api = Api(target_user)
            else:
                logging.info('Impersonation denied. '
                             '{} is not allowed to impersonate {}.'.format(
                                 normal_user.id, target_user.id))
        if not hasattr(self, 'api'):
            # Instantiate an api class that knows about the current user and
            # their permissions. Will default to the permissions of the
            # impersonated user if there is one, or the public user if no one
            # is signed in.
            self.api = Api(self.get_current_user())
        # Also create an internal API that has god-like powers, only to be used
        # by interal platform code, never exposed through URLs.
        self.internal_api = Api(User.create(user_type='god'))
        # We are in the "grandparent" function; do_wrapper() is the "parent",
        # defined in either ApiHandler or ViewHandler; in turn, the "child"
        # function do() will be called.
        self.do_wrapper(*args, **kwargs)

    def post(self, *args, **kwargs):
        """Looks for JSON in the request body; forwards to GET."""
        # Angularjs likes to send content types like text/plain and
        # application/json. Neither will work; webob hard-codes form-based
        # content types, see
        # https://github.com/Pylons/webob/blob/master/webob/request.py#L781
        correct_header = 'application/x-www-form-urlencoded'
        content_type_valid = True
        try:
            content_type = self.request.headers['Content-Type']
        except:
            content_type_valid = False
        else:
            if correct_header not in content_type:
                content_type_valid = False
        if not content_type_valid:
            raise Exception('POSTs must have the header "Content-Type: '
                            'application/x-www-form-urlencoded".')

        # Process JSON data.
        try:  # Client may not send valid JSON.
            # "Manually" interpret the request payload; webob doesn't know how.
            json_payload = json.loads(self.request.body)
        except:
            # This might be a more traditional foo=bar&baz=quz type payload,
            # so leave it alone; webob can interpret it correctly without help.
            pass
        else:
            if type(json_payload) is not dict:
                raise Exception(
                    'POST data must be JSON objects (i.e. dictionaries).')
            # The request object doesn't know how to interpret the JSON in the
            # request body, and so self.request.POST will be full of junk.
            # Luckily, it interprets the POST variables lazily, so we can avoid
            # the junk by clearing the body now.
            self.request.body = ''
            # Now force-feed the POST variables which we have manually
            # interpreted into the request so that future code can just call
            # self.request.get().
            for k, v in json_payload.items():
                self.request.POST[k] = v

        # And finally, now that everything has been massaged, we can continue
        # on to the GET handler.
        self.get(*args, **kwargs)

    def datastore_connected(self):
        """Test if we can reach the datastore."""
        c = DatastoreConnection.get_by_key_name('connection')
        if c:
            logging.info("Confirmed datastore connection: {}.".format(c))
            return True
        elif app_engine_users.is_current_user_admin():
            DatastoreConnection.initialize_connection()
            return True
        else:
            logging.warning("No datastore connection: {}.".format(c))
            return False

    def immaculately_conceive(self):
        """Create and sign in to PERTS as a god user if no other user is signed
        in and you are logged in to google as an app admin."""
        user_id = self.session.get('user')
        if not user_id and app_engine_users.is_current_user_admin():
            google_user = app_engine_users.get_current_user()
            god = User.all().filter('login_email =', google_user.email()).get()
            if not god:
                god = User.create(user_type='god',
                                  name=google_user.nickname(),
                                  login_email=google_user.email(),
                                  auth_id='google_' + google_user.user_id())
                god.put()
            self.session['user'] = god.id

    def get_third_party_auth(self, auth_type):
        """Wrangle and return authentication data from third parties; auth_type
        is either 'google' or 'facebook'.
        Returns a dictionary of user information, which will always contain
        the key 'auth_id'."""
        if auth_type == 'google':
            gae_user = app_engine_users.get_current_user()
            if not gae_user:
                raise CredentialsMissing("No google login found.")
            user_kwargs = {
                'auth_id': 'google_' + gae_user.user_id(),
                'login_email': gae_user.email(),
                # 'name': gae_user.nickname(),
            }
        elif auth_type == 'facebook':
            signed_request = self.facebook_cookie()
            if not signed_request:
                raise CredentialsMissing("No facebook login found.")
            parsed_request = facebook.parse_signed_request(
                signed_request, config.FACEBOOK_APP_SECRET)
            if not parsed_request:
                raise Exception("Invalid facebook authentication.")
            # beware of broken connections to facebook here
            try:
                fb_user = facebook.get_user_from_cookie(
                    self.request.cookies, config.FACEBOOK_APP_ID,
                    config.FACEBOOK_APP_SECRET)
            except IOError:
                fb_user = None
            if fb_user:
                graph = facebook.GraphAPI(fb_user['access_token'])
                profile = graph.get_object('me')
                user_kwargs = {
                    'auth_id': 'facebook_' + fb_user['uid'],
                    'login_email': profile[u'username'] + '@facebook.com',
                    'first_name': profile[u'first_name'],
                    'last_name': profile[u'last_name'],
                    'name': profile[u'first_name'],
                }
            else:
                # The connection between PERTS and facebook is expired or has
                # been used with the GraphAPI already. So just return what we
                # know, which is their facebook user id
                user_kwargs = {
                    'auth_id': 'facebook_' + parsed_request['user_id'],
                }
        else:
            raise Exception("Invalid auth_type: {}.".format(auth_type))
        return user_kwargs

    def facebook_cookie(self):
        cookie_key = 'fbsr_' + config.FACEBOOK_APP_ID
        signed_request = self.request.cookies.get(cookie_key)
        return signed_request

    def authenticate(self, auth_type=None, username=None, password=None):
        """Takes various kinds of credentials (username/password, google
        account, facebook account) and logs you in as a PERTS user.

        Will return one of these three:
        User object:    the user has been successfully authenticated
        False:          credentials invalid, either because a password is wrong
                        or no account exists for those credentials
        None:           looked for credentials but didn't find any of the
                        appropriate kind
        """
        if auth_type is None:
            # determine auth_type if it wasn't provided
            if username is not None and password is not None:
                auth_type = 'direct'
            elif app_engine_users.get_current_user():
                auth_type = 'google'
            elif self.facebook_cookie():
                auth_type = 'facebook'
            else:
                # Auth_type might still be None, which simply means that
                # someone has arrived at login but we don't have any data on
                # them yet. Don't bother with the rest of the function.
                return None

        # fetch matching users
        if auth_type == 'direct':
            if username is None or password is None:
                # no credentials present
                return None

            auth_id = 'direct_' + username.lower()
            user_query_lowercase = User.all().filter('deleted =', False)
            user_query_lowercase.filter('is_test =', False)
            user_query_lowercase.filter('auth_id =', auth_id)
            user_results = user_query_lowercase.fetch(2)

        elif auth_type in ['google', 'facebook']:
            try:
                user_kwargs = self.get_third_party_auth(auth_type)
            except CredentialsMissing:
                return None
            auth_id = user_kwargs['auth_id']
            user_query = User.all().filter('deleted =', False)
            user_query.filter('auth_id =', auth_id)
            # Fetch 2 b/c that's sufficient to detect multiple matching users.
            user_results = user_query.fetch(2)

        # interpret the results of the query
        num_matches = len(user_results)
        if num_matches is 0:
            # no users with that username, invalid log in
            return False
        elif num_matches > 1:
            logging.error(
                "More than one user matches auth info: {}".format(auth_id))
            # Sort the users by modified time, so we take the most recently
            # modified one.
            user_results = sorted(user_results,
                                  key=lambda u: u.modified,
                                  reverse=True)
        # else num_matches is 1, the default case, and we can assume there was
        # one matching user
        user = user_results[0]

        # For direct authentication, PERTS is in charge of
        # checking their credentials, so validate the password.
        if auth_type == 'direct':
            # A user-specific salt AND how many "log rounds" (go read about key
            # stretching) should be used is stored IN the user's hashed
            # password; that's why it's an argument here.
            # http://pythonhosted.org/passlib/
            if not sha256_crypt.verify(password, user.hashed_password):
                # invalid password for this username
                return False

        # We've decided our response; if the user has successfully
        # authenticated, then log them in to the session.
        if isinstance(user, User):
            # all's well, log them in and return the matching user
            self.session['user'] = user.id
        return user

    def _base_identify_query(self, classroom_id):
        """The various ways we look for students have these similarities."""
        query = User.all().filter('deleted =', False)
        query.filter('is_test =', False)
        query.filter('user_type =', 'student')
        query.filter('assc_classroom_list =', classroom_id)
        return query

    def _identify_exact_matches(self, classroom_id, first_name, last_name):
        """Search for exact matches to identify students.

        "Exact" means match on first name, last name, and classroom.
        """
        stripped_first_name = util.clean_string(first_name)
        stripped_last_name = util.clean_string(last_name)

        logging.info(
            "Querying for exact match on is_test: False, user_type: student, "
            "classroom: {}, stripped_first_name: {}, stripped_last_name: {}".
            format(classroom_id, stripped_first_name, stripped_last_name))

        # Query based on stripped names because we expect students to type
        # their name differently from session to session. Stripping attempts
        # to make their name uniform and still unique. See util.clean_string().
        stripped_q = self._base_identify_query(classroom_id)
        stripped_q.filter('stripped_first_name =', stripped_first_name)
        stripped_q.filter('stripped_last_name =', stripped_last_name)

        return stripped_q.fetch(5)

    def _identify_partial_matches(self, cohort_id, last_name):
        """Search for partial matches to identify students.

        Pulls data from a special set of memcache keys, which are updated by
        cron, and provide the names of all students in the school. All the
        names are examined to see if the typed name is contained in or
        contained by the existing name ("containment matching"), which are
        considered partial matches. Then the matches are ordered by their
        similarity (Levenshtein distance) to the typed name.
        """
        stripped_last_name = util.clean_string(last_name)

        match_data, from_memcache = self.internal_api.get_roster(cohort_id)

        # White list necessary properties (no sense in releasing status codes
        # like 'Parent Refusal' to the public).
        def clean_properties(d):
            white_list = [
                'first_name', 'last_name', 'classroom_name', 'id',
                'stripped_last_name'
            ]
            return {k: v for k, v in d.items() if k in white_list}

        # Containment matching.
        matches = [
            clean_properties(u) for u in match_data
            if u['stripped_last_name'] in stripped_last_name
            or stripped_last_name in u['stripped_last_name']
        ]

        # Order by edit (Levenshtein) distance from the submitted name.
        sort_func = lambda n: util.levenshtein_distance(
            n['stripped_last_name'], stripped_last_name)
        return sorted(matches, key=sort_func)

    def _create_student(self, classroom, first_name, last_name):

        new_student = self.api.create(
            'user', {
                'first_name': first_name,
                'last_name': last_name,
                'user_type': 'student',
                'classroom': classroom.id,
            })

        return new_student

    def _log_in_student(self, classroom, user):
        """Students always log in in the context of a classroom."""
        # Nota bene: if this is a newly created user, they will still
        # have registration_complete as false, and so we can intelligently
        # redirect them to a page handling consent, race, gender, etc.
        self.session['user'] = user.id
        # If this user isn't already associated with this
        # classroom, do it now
        # todo: remove other existing classrooms associations?
        if classroom.id not in user.assc_classroom_list:
            user = self.api.associate('associate', user, classroom)

    def identify(self,
                 cohort=None,
                 classroom=None,
                 first_name=None,
                 last_name=None,
                 force_create=False):
        """Takes a student's identifying information and attempts to match
        them with other known students.

        If none can be found, a new student user is created. If only last name
        and birth date match existing users, partial matches are sent back for
        verification. Identified students are logged in. If an existing student
        logs in with a classroom id that is new for them, it is simply added to
        their list of associated classrooms.

        The force_create parameter is used for user's response to the partial
        match result. If they declare none of those partial matches to be
        themselves, we force the creation of a new user.

        23 April 2014 - ajb
        Changing the identify process to *only* create a new user when
        the force_create variable is specified to be true. Instead of
        creating a new user when no other users are found, set the
        response['new_user'] to true a

        """
        logging.info(
            u'BaseHandler.identify(cohort={}, classroom={}, first_name={}, '
            'last_name={}, force_create={})'.format(cohort, classroom,
                                                    first_name, last_name,
                                                    force_create))

        response = {
            'exact_match': '',
            'partial_matches': [],
            'new_user': False
        }

        if force_create:
            # Check for the student one more time, since we've given them the
            # opportunity to double-check and edit their name.
            exact_matches = self._identify_exact_matches(
                classroom.id, first_name, last_name)
            if len(exact_matches) is 1:
                user = exact_matches[0]
            else:
                user = self._create_student(classroom, first_name, last_name)
            response['exact_match'] = user.id
            self._log_in_student(classroom, user)
            return response

        exact_matches = self._identify_exact_matches(classroom.id, first_name,
                                                     last_name)

        num_exact = len(exact_matches)
        if num_exact > 1:
            # This is a problem; we have multiple identical users.
            # Return the newest version of this user, b/c it's likely that
            # one which will be in their session and have the most up-to-
            # date data.
            newest_to_oldest = sorted(exact_matches,
                                      key=lambda e: e.created,
                                      reverse=True)
            user = newest_to_oldest[0]
            logging.error(u'Multiple identical students: {} {}.'.format(
                user.stripped_first_name, user.stripped_last_name))
            # Still allow the user to sign in, however:
            response['exact_match'] = user.id
        elif num_exact is 1:
            # Great, a unique match.
            user = exact_matches[0]
            logging.info('Exact match found: {}'.format(user.id))
            response['exact_match'] = user.id
        else:

            # No exact matches found. We'll have to loosen our requirements.
            logging.info('No exact matches found.')

            partial_matches = self._identify_partial_matches(
                cohort.id, last_name)

            num_partial = len(partial_matches)
            if num_partial > 0:
                # Only partial matches exist; we'll have to ask the user about
                # them. Include some details on those matches so the user can
                # pick between them intelligently.
                logging.info('Partial matches found: {}'.format(
                    [u['id'] for u in partial_matches]))
                response['partial_matches'] = partial_matches
            else:  # i.e., if num_partial == 0:
                # No matching users found at all! Create one.
                logging.info(
                    'No matches found. Starting new user double check process.'
                )
                response['new_user'] = True

        if response['exact_match'] != '':
            # Then we can expect a user has been found. Log them in.
            self._log_in_student(classroom, user)

        return response

    def register(self, auth_type, username=None, password=None):
        """Logs in users and registers them if they're new.

        Raises exceptions when the system appears to be broken (e.g. redundant
        users).

        Returns tuple(
            user - mixed, False when given non-sensical data by users (e.g.
                registering an existing email under a new auth type) or
                matching user entity,
            user_is_new - bool, True if user was newly registered
        )

        """
        user_is_new = False

        if auth_type not in config.allowed_auth_types:
            raise Exception("Bad auth_type: {}.".format(auth_type))
        if auth_type == 'direct':
            if None in [username, password]:
                raise Exception("Credentials incomplete.")
            # Because app engine is case sensitive, and we can't predict what
            # case users will use when entering their email, force everything
            # to lower case. See issue #208.
            creation_kwargs = {
                'login_email': username,
                'auth_id': 'direct_' + username,
                'plaintext_password': password,  # it WILL be hashed later
            }

        # These are the third party identity providers we currently know
        # how to handle. See util_handlers.BaseHandler.get_third_party_auth().
        elif auth_type in ['google', 'facebook']:
            # may raise CredentialsMissing
            creation_kwargs = self.get_third_party_auth(auth_type)

        # Check that the user hasn't already registered in two ways.

        # 1) If the email matches but the auth type is different, we reject
        #    the request to register so the UI can warn the user.
        email_match_params = {'login_email': creation_kwargs['login_email']}
        email_matches = self.internal_api.get('user', email_match_params)
        for user in email_matches:
            if user.auth_type() != auth_type:
                user = False
                return (user, user_is_new)

        # 2) If the auth_id matches, they tried to register when they should
        #    have logged in. Just log them in.
        auth_match_params = {'auth_id': creation_kwargs['auth_id']}
        auth_matches = self.internal_api.get('user', auth_match_params)
        if len(auth_matches) == 1:
            # they already have an account
            user = auth_matches[0]

        # This user hasn't already registered. Register them.
        elif len(auth_matches) == 0:
            user_is_new = True
            # having collected the user's information, build a user object
            creation_kwargs['user_type'] = 'teacher'
            user = self.api.create('user', creation_kwargs)
            # send them an email to confirm that they have registered
            # and email them about it
            email = Email.create(to_address=creation_kwargs['login_email'],
                                 reply_to=config.registration_email_from,
                                 from_address=config.registration_email_from,
                                 subject=config.registration_email_subject,
                                 body=config.registration_email_body.format(
                                     creation_kwargs['login_email']))

            logging.info('url_handlers.BaseHandler.register()')
            logging.info('sending an email to: {}'.format(
                creation_kwargs['login_email']))
            Email.send(email)

        # There's a big problem.
        else:
            logging.error("Two matching users! {}".format(auth_match_params))
            # Sort the users by modified time, so we take the most recently
            # modified one.
            auth_matches = sorted(auth_matches,
                                  key=lambda u: u.modified,
                                  reverse=True)
            user = auth_matches[0]

        # Sign in the user.
        self.session['user'] = user.id

        return (user, user_is_new)

    def impersonate(self, target):
        """Set a special user id in the session so get_current_user() returns
        that user. Makes the website look like the impersonate user would see
        it, while the original user remains logged in. Raises
        PermissionDenied."""
        normal_user = self.get_current_user(method='normal')
        if normal_user.can_impersonate(target):
            # set the impersonated user
            self.session['impersonated_user'] = target.id
        else:
            raise PermissionDenied("Not allowed to impersonate {}".format(
                target.id))

    def stop_impersonating(self):
        self.session['impersonated_user'] = None

    def clean_up_users(self, session_key):
        """Brings the three representations of users into alignment: the entity
        in the datastore, the id in the session, and the cached object saved as
        a property of the request handler."""
        id = self.session.get(session_key)
        attr = '_' + session_key
        cached_user = getattr(self, attr)
        if not id:
            # clear the cached user b/c the session is empty
            setattr(self, attr, None)
        elif not cached_user or id != cached_user.id:
            # the cached user is invalid, try to restore from the session...
            datastore_user = User.get_by_id(id)
            if datastore_user:
                # found the user defined by the session; cache them
                setattr(self, attr, datastore_user)
            else:
                # could NOT find the user; clear everything
                del self.session[session_key]
                setattr(self, attr, None)
        # Make sure the session keys always exist, even if they are empty.
        if session_key not in self.session:
            self.session[session_key] = None

    def get_current_user(self, method=None):
        """Get the currently logged in PERTS user with the following priority:
            1. Impersonated user
            2. Normal user
            3. Public user
        The method argument can override this behavior. If overriding and the
        requested type of user is not present, will return the public user.
        """
        public_user = User(user_type='public')

        # Check that the session matches the cached user entites.
        self.clean_up_users('user')
        self.clean_up_users('impersonated_user')

        # return what was asked
        if method == 'normal':
            return self._user or public_user
        elif method == 'impersonated':
            return self._impersonated_user or public_user
        else:
            return self._impersonated_user or self._user or public_user

    def dispatch(self):
        """Initialize and manage sessions."""
        # Get a session store for this request.
        self.session_store = sessions.get_store(request=self.request)
        try:
            # Dispatch the request.
            webapp2.RequestHandler.dispatch(self)
        finally:
            # Save all sessions.
            self.session_store.save_sessions(self.response)

    @webapp2.cached_property
    def session(self):
        """Allows set/get of session data within handler methods.
        To set a value: self.session['foo'] = 'bar'
        To get a value: foo = self.session.get('foo')"""
        # Returns a session based on a cookie. Other options are 'datastore'
        # and 'memcache', which may be useful if we continue to have bugs
        # related to dropped sessions. Setting the name is critical, because it
        # allows use to delete the cookie during logout.
        # http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html
        return self.session_store.get_session(name='perts_login',
                                              backend='securecookie')