def delete(self, user_uuid, third_party_user_uuid): user = User.get_by_uuid_or_404(user_uuid) third_party_user = ThirdPartyUser.get_by_uuid_or_404( third_party_user_uuid) require( MANAGE, User, title="User's Third Party Logins Unavailable", message= "Sorry, your system role does not allow you to delete third party connections for this user." ) on_user_third_party_user_delete.send( self, event_name=on_user_third_party_user_delete.name, user=current_user, data={ 'user_id': user.id, 'third_party_type': third_party_user.third_party_type, 'unique_identifier': third_party_user.unique_identifier }) # TODO: consider adding soft delete to thrid_party_user in the future ThirdPartyUser.query.filter( ThirdPartyUser.uuid == third_party_user_uuid).delete() db.session.commit() return {'success': True}
def delete(self, user_uuid, third_party_user_uuid): user = User.get_by_uuid_or_404(user_uuid) third_party_user = ThirdPartyUser.get_by_uuid_or_404(third_party_user_uuid) require(MANAGE, User, title="User's Third Party Logins Unavailable", message="Sorry, your system role does not allow you to delete third party connections for this user.") on_user_third_party_user_delete.send( self, event_name=on_user_third_party_user_delete.name, user=current_user, data={'user_id': user.id, 'third_party_type': third_party_user.third_party_type, 'unique_identifier': third_party_user.unique_identifier}) # TODO: consider adding soft delete to thrid_party_user in the future ThirdPartyUser.query.filter(ThirdPartyUser.uuid == third_party_user_uuid).delete() db.session.commit() return { 'success': True }
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)) }
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)
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)
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())