Example #1
0
def import_users(import_type, course, users):
    invalids = []  # invalid entries - eg. invalid # of columns
    count = 0  # store number of successful enrolments

    imported_users = []
    set_user_passwords = []

    # store unique user identifiers - eg. student number - throws error if duplicate in file
    import_usernames = []
    import_student_numbers = []

    # store unique user identifiers - eg. student number - throws error if duplicate in file
    existing_system_usernames = _get_existing_users_by_identifier(
        import_type, users)
    existing_system_student_numbers = _get_existing_users_by_student_number(
        import_type, users)

    groups = course.groups.all()
    groups_by_name = {}
    for group in groups:
        groups_by_name[group.name] = group

    # create / update users in file
    for user_row in users:
        if len(user_row) < 1:
            continue  # skip empty row
        user = _parse_user_row(import_type, user_row)

        # validate unique identifier
        username = user.get('username')
        password = user.get(
            'password'
        )  #always None for CAS/SAML import, can be None for existing users on ComPAIR import
        student_number = user.get('student_number')

        u = existing_system_usernames.get(username, None)

        if not username:
            invalids.append({
                'user': User(username=username),
                'message': 'The username is required.'
            })
            continue
        elif username in import_usernames:
            invalids.append({
                'user':
                User(username=username),
                'message':
                'This username already exists in the file.'
            })
            continue

        if u:
            # overwrite password if user has not logged in yet
            if u.last_online == None and not password in [None, '*']:
                set_user_passwords.append((u, password))
        else:
            u = User(username=None,
                     password=None,
                     student_number=user.get('student_number'),
                     firstname=user.get('firstname'),
                     lastname=user.get('lastname'),
                     email=user.get('email'))
            if import_type == ThirdPartyType.cas.value or import_type == ThirdPartyType.saml.value:
                # CAS/SAML login
                u.third_party_auths.append(
                    ThirdPartyUser(
                        unique_identifier=username,
                        third_party_type=ThirdPartyType(import_type)))
            else:
                # ComPAIR login
                u.username = username
                if password in [None, '*']:
                    invalids.append({
                        'user': u,
                        'message': 'The password is required.'
                    })
                    continue
                elif len(password) < 4:
                    invalids.append({
                        'user':
                        u,
                        'message':
                        'The password must be at least 4 characters long.'
                    })
                    continue
                else:
                    set_user_passwords.append((u, password))

            # validate student number (if not None)
            if student_number:
                # invalid if already showed up in file
                if student_number in import_student_numbers:
                    u.username = username
                    invalids.append({
                        'user':
                        u,
                        'message':
                        'This student number already exists in the file.'
                    })
                    continue
                # invalid if student number already exists in the system
                elif student_number in existing_system_student_numbers:
                    u.username = username
                    invalids.append({
                        'user':
                        u,
                        'message':
                        'This student number already exists in the system.'
                    })
                    continue

            u.system_role = SystemRole.student
            u.displayname = user.get('displayname') if user.get(
                'displayname') else display_name_generator()
            db.session.add(u)

        import_usernames.append(username)
        if student_number:
            import_student_numbers.append(student_number)
        imported_users.append((u, user.get('group')))
    db.session.commit()

    enroled = UserCourse.query \
        .filter_by(course_id=course.id) \
        .all()

    enroled = {e.user_id: e for e in enroled}

    students = UserCourse.query \
        .filter_by(
            course_id=course.id,
            course_role=CourseRole.student
        ) \
        .all()
    students = {s.user_id: s for s in students}

    # enrol valid users in file
    for user, group_name in imported_users:
        enrol = enroled.get(user.id,
                            UserCourse(course_id=course.id, user_id=user.id))
        enrol.group = None
        if group_name:
            group = groups_by_name.get(group_name)
            # add new groups if needed
            if not group:
                group = Group(course=course, name=group_name)
                groups_by_name[group_name] = group
                db.session.add(group)
            enrol.group = group

        # do not overwrite instructor or teaching assistant roles
        if enrol.course_role not in [
                CourseRole.instructor, CourseRole.teaching_assistant
        ]:
            enrol.course_role = CourseRole.student
            if user.id in students:
                del students[user.id]
            count += 1
        db.session.add(enrol)

    db.session.commit()

    # unenrol users not in file anymore
    for user_id in students:
        enrolment = students.get(user_id)
        # skip users that are already dropped
        if enrolment.course_role == CourseRole.dropped:
            continue
        enrolment.course_role = CourseRole.dropped
        enrolment.group_id = None
        db.session.add(enrolment)
    db.session.commit()

    # wait until user ids are generated before starting background jobs
    # perform password update in chunks of 100
    chunk_size = 100
    chunks = [
        set_user_passwords[index:index + chunk_size]
        for index in range(0, len(set_user_passwords), chunk_size)
    ]
    for chunk in chunks:
        set_passwords.delay({user.id: password for (user, password) in chunk})

    on_classlist_upload.send(current_app._get_current_object(),
                             event_name=on_classlist_upload.name,
                             user=current_user,
                             course_id=course.id)

    return {
        'success': count,
        'invalids': marshal(invalids,
                            dataformat.get_import_users_results(False))
    }
Example #2
0
def cas_auth():
    """
    CAS Authentication Endpoint. Authenticate user through CAS.
    If error, set message in session so that frontend can get the message through /session call
    """
    if not current_app.config.get('CAS_LOGIN_ENABLED'):
        abort(
            403,
            title="Log In Failed",
            message=
            "Please try an alternate way of logging in. The CWL login has been disabled by your system administrator."
        )

    url = "/app/#/lti" if sess.get('LTI') else "/"
    error_message = None
    ticket = request.args.get("ticket")

    # check if token isn't present
    if not ticket:
        error_message = "No token in request"
    else:
        validation_response = validate_cas_ticket(ticket)

        if not validation_response.success:
            current_app.logger.debug(
                "CAS Server did NOT validate ticket:%s and included this response:%s"
                % (ticket, validation_response.response))
            error_message = "Login Failed. CAS ticket was invalid."
        elif not validation_response.user:
            current_app.logger.debug(
                "CAS Server responded with valid ticket but no user")
            error_message = "Login Failed. Expecting CAS username to be set."
        else:
            current_app.logger.debug(
                "CAS Server responded with user:%s and attributes:%s" %
                (validation_response.user, validation_response.attributes))
            username = validation_response.user

            thirdpartyuser = ThirdPartyUser.query. \
                filter_by(
                    unique_identifier=username,
                    third_party_type=ThirdPartyType.cas
                ) \
                .one_or_none()

            if not thirdpartyuser:
                thirdpartyuser = ThirdPartyUser(
                    unique_identifier=username,
                    third_party_type=ThirdPartyType.cas,
                    params=validation_response.attributes)
                thirdpartyuser.generate_or_link_user_account()
                db.session.add(thirdpartyuser)
                db.session.commit()
            elif not thirdpartyuser.user:
                thirdpartyuser.generate_or_link_user_account()
                db.session.commit()

            authenticate(thirdpartyuser.user,
                         login_method=thirdpartyuser.third_party_type.value)
            thirdpartyuser.params = validation_response.attributes

            if sess.get('LTI') and sess.get('lti_create_user_link'):
                lti_user = LTIUser.query.get_or_404(sess['lti_user'])
                lti_user.compair_user_id = thirdpartyuser.user_id
                lti_user.upgrade_system_role()
                lti_user.update_user_profile()
                sess.pop('lti_create_user_link')
            else:
                thirdpartyuser.upgrade_system_role()
                thirdpartyuser.update_user_profile()

            if sess.get('LTI') and sess.get('lti_context') and sess.get(
                    'lti_user_resource_link'):
                lti_context = LTIContext.query.get_or_404(sess['lti_context'])
                lti_user_resource_link = LTIUserResourceLink.query.get_or_404(
                    sess['lti_user_resource_link'])
                lti_context.update_enrolment(
                    thirdpartyuser.user_id, lti_user_resource_link.course_role)

            db.session.commit()
            sess['CAS_LOGIN'] = True

    if error_message is not None:
        sess['THIRD_PARTY_AUTH_ERROR_TYPE'] = 'CAS'
        sess['THIRD_PARTY_AUTH_ERROR_MSG'] = error_message

    return redirect(url)
Example #3
0
def saml_auth():
    """
    SAML Authentication Endpoint. Authenticate user through SAML.
    If error, set message in session so that frontend can get the message through /session call
    """
    if not current_app.config.get('SAML_LOGIN_ENABLED'):
        abort(
            403,
            title="Not Logged In",
            message=
            "Please use a valid way to log in. You are not able to use CWL login based on the current settings."
        )

    url = "/app/#/lti" if sess.get('LTI') else "/"
    error_message = None
    auth = get_saml_auth_response(request)
    errors = auth.get_errors()

    if len(errors) > 0:
        current_app.logger.debug("SAML IdP returned errors: %s" % (errors))
        error_message = "Login Failed."
    elif not auth.is_authenticated():
        current_app.logger.debug("SAML IdP not logged in")
        error_message = "Login Failed."
    else:
        attributes = auth.get_attributes()
        unique_identifier = attributes.get(
            current_app.config.get('SAML_UNIQUE_IDENTIFIER'))
        current_app.logger.debug("SAML IdP responded with attributes:%s" %
                                 (attributes))

        if not unique_identifier or (isinstance(unique_identifier, list)
                                     and len(unique_identifier) == 0):
            current_app.logger.error(
                "SAML idP did not return the unique_identifier " +
                current_app.config.get('SAML_UNIQUE_IDENTIFIER') +
                " within its attributes")
            error_message = "Login Failed. Expecting " + current_app.config.get(
                'SAML_UNIQUE_IDENTIFIER') + " to be set."
        else:
            # set unique_identifier to first item in list if unique_identifier is an array
            if isinstance(unique_identifier, list):
                unique_identifier = unique_identifier[0]

            thirdpartyuser = ThirdPartyUser.query. \
                filter_by(
                    unique_identifier=unique_identifier,
                    third_party_type=ThirdPartyType.saml
                ) \
                .one_or_none()

            sess['SAML_NAME_ID'] = auth.get_nameid()
            sess['SAML_SESSION_INDEX'] = auth.get_session_index()

            if not thirdpartyuser:
                thirdpartyuser = ThirdPartyUser(
                    unique_identifier=unique_identifier,
                    third_party_type=ThirdPartyType.saml,
                    params=attributes)
                thirdpartyuser.generate_or_link_user_account()
                db.session.add(thirdpartyuser)
                db.session.commit()
            elif not thirdpartyuser.user:
                thirdpartyuser.generate_or_link_user_account()
                db.session.commit()

            authenticate(thirdpartyuser.user,
                         login_method=thirdpartyuser.third_party_type.value)
            thirdpartyuser.params = attributes

            if sess.get('LTI') and sess.get('lti_create_user_link'):
                lti_user = LTIUser.query.get_or_404(sess['lti_user'])
                lti_user.compair_user_id = thirdpartyuser.user_id
                lti_user.upgrade_system_role()
                lti_user.update_user_profile()
                sess.pop('lti_create_user_link')
            else:
                thirdpartyuser.upgrade_system_role()
                thirdpartyuser.update_user_profile()

            if sess.get('LTI') and sess.get('lti_context') and sess.get(
                    'lti_user_resource_link'):
                lti_context = LTIContext.query.get_or_404(sess['lti_context'])
                lti_user_resource_link = LTIUserResourceLink.query.get_or_404(
                    sess['lti_user_resource_link'])
                lti_context.update_enrolment(
                    thirdpartyuser.user_id, lti_user_resource_link.course_role)

            db.session.commit()
            sess['SAML_LOGIN'] = True

    if error_message is not None:
        sess['THIRD_PARTY_AUTH_ERROR_TYPE'] = 'SAML'
        sess['THIRD_PARTY_AUTH_ERROR_MSG'] = error_message

    return redirect(url)
Example #4
0
    def post(self):
        # login_required when oauth_create_user_link not set
        if not sess.get('oauth_create_user_link'):
            if not current_app.login_manager._login_disabled and \
                    not current_user.is_authenticated:
                return current_app.login_manager.unauthorized()

        user = User()
        params = new_user_parser.parse_args()
        user.student_number = params.get("student_number", None)
        user.email = params.get("email")
        user.firstname = params.get("firstname")
        user.lastname = params.get("lastname")
        user.displayname = params.get("displayname")

        email_notification_method = params.get("email_notification_method")
        check_valid_email_notification_method(email_notification_method)
        user.email_notification_method = EmailNotificationMethod(
            email_notification_method)

        # if creating a cas user, do not set username or password
        if sess.get('oauth_create_user_link') and sess.get('LTI') and sess.get(
                'CAS_CREATE'):
            user.username = None
            user.password = None
        else:
            # else enforce required password and unique username
            user.password = params.get("password")
            if user.password == None:
                abort(
                    400,
                    title="User Not Saved",
                    message=
                    "A password is required. Please enter a password and try saving again."
                )

            user.username = params.get("username")
            if user.username == None:
                abort(
                    400,
                    title="User Not Saved",
                    message=
                    "A username is required. Please enter a username and try saving again."
                )

            username_exists = User.query.filter_by(
                username=user.username).first()
            if username_exists:
                abort(
                    409,
                    title="User Not Saved",
                    message=
                    "Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again."
                )

        student_number_exists = User.query.filter_by(
            student_number=user.student_number).first()
        # if student_number is not left blank and it exists -> 409 error
        if user.student_number is not None and student_number_exists:
            abort(
                409,
                title="User Not Saved",
                message=
                "Sorry, this student number already exists and student numbers must be unique in ComPAIR. Please enter another number and try saving again."
            )

        # handle oauth_create_user_link setup for third party logins
        if sess.get('oauth_create_user_link'):
            login_method = None

            if sess.get('LTI'):
                lti_user = LTIUser.query.get_or_404(sess['lti_user'])
                lti_user.compair_user = user
                user.system_role = lti_user.system_role
                login_method = 'LTI'

                if sess.get('lti_context') and sess.get(
                        'lti_user_resource_link'):
                    lti_context = LTIContext.query.get_or_404(
                        sess['lti_context'])
                    lti_user_resource_link = LTIUserResourceLink.query.get_or_404(
                        sess['lti_user_resource_link'])
                    if lti_context.is_linked_to_course():
                        # create new enrollment
                        new_user_course = UserCourse(
                            user=user,
                            course_id=lti_context.compair_course_id,
                            course_role=lti_user_resource_link.course_role)
                        db.session.add(new_user_course)

                if sess.get('CAS_CREATE'):
                    thirdpartyuser = ThirdPartyUser(
                        third_party_type=ThirdPartyType.cas,
                        unique_identifier=sess.get('CAS_UNIQUE_IDENTIFIER'),
                        params=sess.get('CAS_PARAMS'),
                        user=user)
                    login_method = ThirdPartyType.cas.value
                    db.session.add(thirdpartyuser)
        else:
            system_role = params.get("system_role")
            check_valid_system_role(system_role)
            user.system_role = SystemRole(system_role)

            require(
                CREATE,
                user,
                title="User Not Saved",
                message="Sorry, your role does not allow you to save users.")

        # only students can have student numbers
        if user.system_role != SystemRole.student:
            user.student_number = None

        try:
            db.session.add(user)
            db.session.commit()
            if current_user.is_authenticated:
                on_user_create.send(self,
                                    event_name=on_user_create.name,
                                    user=current_user,
                                    data=marshal(user,
                                                 dataformat.get_user(False)))
            else:
                on_user_create.send(self,
                                    event_name=on_user_create.name,
                                    data=marshal(user,
                                                 dataformat.get_user(False)))

        except exc.IntegrityError:
            db.session.rollback()
            current_app.logger.error("Failed to add new user. Duplicate.")
            abort(
                409,
                title="User Not Saved",
                message=
                "Sorry, this ID already exists and IDs must be unique in ComPAIR. Please try addding another user."
            )

        # handle oauth_create_user_link teardown for third party logins
        if sess.get('oauth_create_user_link'):
            authenticate(user, login_method=login_method)
            sess.pop('oauth_create_user_link')

            if sess.get('CAS_CREATE'):
                sess.pop('CAS_CREATE')
                sess.pop('CAS_UNIQUE_IDENTIFIER')
                sess['CAS_LOGIN'] = True

        return marshal(user, dataformat.get_user())