def test_login_duplicate_email(test_client, session, error_template, request, app): new_users = [ m.User(name='NEW_USER', email='*****@*****.**', password='******', active=True, username='******'), m.User(name='NEW_USER', email='*****@*****.**', password='******', active=True, username='******') ] for new_user in new_users: session.add(new_user) session.commit() for user_id in [u.id for u in new_users]: user = LocalProxy(lambda: m.User.query.get(user_id)) with app.app_context(): res = test_client.req('post', f'/api/v1/login', 200, data={ 'username': user.username, 'password': '******' }, result={ 'user': { 'email': '*****@*****.**', 'id': int, 'name': 'NEW_USER', 'username': user.username, 'hidden': False, 'group': None, 'is_test_student': False, }, 'access_token': str, }) access_token = res['access_token'] with app.app_context(): test_client.req( 'get', '/api/v1/login', 200, headers={'Authorization': f'Bearer {access_token}'}, result={ 'username': user.username, 'id': int, 'name': user.name, 'group': None, 'is_test_student': False, }) with app.app_context(): test_client.req('get', '/api/v1/login', 401)
def test_password_strength_on_login(test_client, session, app, monkeypatch, password, is_strong): monkeypatch.setitem(app.config, 'MIN_PASSWORD_SCORE', 0) new_user = m.User( name='NEW_USER', email='*****@*****.**', password=password, active=True, username='******', ) session.add(new_user) session.commit() monkeypatch.setitem(app.config, 'MIN_PASSWORD_SCORE', 3) res, rv = test_client.req( 'post', '/api/v1/login', 200, data={ 'username': new_user.username, 'password': password, }, result=dict, include_response=True, ) if is_strong: assert 'warning' not in rv.headers else: assert 'warning' in rv.headers
def test_login(test_client, session, error_template, password, request, active, app, username): new_user = m.User( name='NEW_USER', email='*****@*****.**', password='******', active=active, username='******', role=session.query(m.Role).first(), ) session.add(new_user) session.commit() data_err = request.node.get_closest_marker('data_error') if data_err: error = 400 if data_err.kwargs.get('wrong'): error_template = copy.deepcopy(error_template) error_template[ 'message'] == 'The supplied email or password is wrong.' elif not active: error = 403 else: error = False data = {} if password is not None: data['password'] = password if username is not None: data['username'] = username with app.app_context(): res = test_client.req('post', f'/api/v1/login?with_permissions', error or 200, data=data, result=error_template if error else { 'user': { 'email': '*****@*****.**', 'id': int, 'name': 'NEW_USER', 'username': '******', 'hidden': False, 'permissions': dict, 'group': None, 'is_test_student': False, }, 'access_token': str }) access_token = '' if error else res['access_token'] with app.app_context(): test_client.req('get', '/api/v1/login', 401 if error else 200, headers={'Authorization': f'Bearer {access_token}'}) with app.app_context(): test_client.req('get', '/api/v1/login', 401)
def test_update_user_info( logged_in, test_client, session, new_password, email, name, old_password, error_template, request, role ): user = m.User( name='NEW_USER', email='*****@*****.**', password='******', active=True, username='******', role=m.Role.query.filter_by(name=role).one(), ) session.add(user) session.commit() user_id = user.id missing_err = request.node.get_closest_marker('missing_error') data_err = request.node.get_closest_marker('data_error') perm_err = request.node.get_closest_marker('perm_error') password_err = request.node.get_closest_marker('password_error') needs_pw = request.node.get_closest_marker('needs_password') if missing_err: error = 400 elif perm_err: error = 403 elif password_err: error = 403 elif needs_pw and old_password != 'a': error = 403 elif data_err: error = 400 else: error = False data = {} if new_password is not None: data['new_password'] = new_password if old_password is not None: data['old_password'] = old_password if email is not None: data['email'] = email if name is not None: data['name'] = name with logged_in(user): test_client.req( 'patch', '/api/v1/login', error or 200, data=data, ) new_user = m.User.query.get(user_id) if not error: assert new_user.name == name assert new_user.email == email else: assert new_user.name != name
def create_user_with_role(session, role, courses, name=None): if not isinstance(courses, list): courses = [courses] n_id = str(uuid.uuid4()) new_role = m.Role(name=f'NEW_ROLE--{n_id}') user = m.User( name=f'NEW_USER-{n_id}' if name is None else name, email=f'new_user-{n_id}@a.nl', password=n_id, active=True, username=f'a-the-a-er-{n_id}' if name is None else f'{name}{n_id}', role=new_role, ) for course in courses: user.courses[get_id(course)] = m.CourseRole.query.filter_by( name=role, course_id=get_id(course) ).one() session.add(user) session.commit() u_id = user.id return LocalProxy(lambda: m.User.query.get(u_id))
def test_update_user_info_permissions(logged_in, test_client, session, error_template, request): new_role = m.Role(name='NEW_ROLE') info_perm = psef.permissions.GlobalPermission.can_edit_own_info pw_perm = psef.permissions.GlobalPermission.can_edit_own_password new_role.set_permission(info_perm, False) new_role.set_permission(pw_perm, False) session.add(new_role) user = m.User( name='NEW_USER', email='*****@*****.**', password='******', active=True, username='******', role=new_role, ) session.add(user) session.commit() user_id = user.id data = {} data['new_password'] = '******' data['old_password'] = '******' data['email'] = '*****@*****.**' data['name'] = 'new_name' with logged_in(user): # This user has no permissions so it should not be possible to do this. test_client.req( 'patch', '/api/v1/login', 403, data=data, result=error_template, ) pw_perm = GlobalPermission.can_edit_own_password m.User.query.get(user_id).role.set_permission(pw_perm, True) session.commit() # This user does not have the permission to change the name, so it # should fail test_client.req( 'patch', '/api/v1/login', 403, data=data, result=error_template, ) # However only password should be good test_client.req( 'patch', '/api/v1/login', 200, data={ 'name': 'NEW_USER', 'email': '*****@*****.**', 'old_password': '******', 'new_password': '******' }, ) pw_perm = psef.permissions.GlobalPermission.can_edit_own_password info_perm = psef.permissions.GlobalPermission.can_edit_own_info m.User.query.get(user_id).role.set_permission(pw_perm, False) m.User.query.get(user_id).role.set_permission(info_perm, True) session.commit() # This user does not have the permission to change the pw, so it # should fail test_client.req( 'patch', '/api/v1/login', 403, data=data, result=error_template, ) # However only name should be good test_client.req( 'patch', '/api/v1/login', 200, data={ 'name': 'new_name1', 'email': '*****@*****.**', 'old_password': '', 'new_password': '', }, ) m.User.query.get(user_id).role.set_permission( GlobalPermission.can_edit_own_password, True) session.commit() # It now has both so this should work. test_client.req( 'patch', '/api/v1/login', 403, data=data, result=error_template, )
def test_login_rate_limit(test_client, session, describe, app): with describe('setup'): password = str(uuid.uuid4()) new_user1 = m.User( name='NEW_USER', email='*****@*****.**', password=password, active=True, username='******', role=session.query(m.Role).first(), ) new_user2 = m.User( name='NEW_USER', email='*****@*****.**', password=password, active=True, username='******', role=session.query(m.Role).first(), ) session.add(new_user1) session.add(new_user2) session.commit() with describe('Will get rate limit if trying too much'): for i in range(100): with app.app_context(): res = test_client.post( '/api/v1/login?with_permissions', json={'username': '******', 'password': '******'} ) if res.status_code == 429: assert i >= 5 break else: assert res.status_code >= 400 else: assert False, 'Expected 429 at some point' with describe('Can still login for another user'): with app.app_context(): res = test_client.req( 'post', '/api/v1/login?with_permissions', 200, data={ 'username': '******', 'password': password, }, ) with describe('Correct login does not fix rate limit'): with app.app_context(): res = test_client.req( 'post', '/api/v1/login?with_permissions', 429, data={ 'username': '******', 'password': password, }, )
def test_data(db=None): db = db or psef.models.db if not app.config['DEBUG']: print('You can not add test data in production mode', file=sys.stderr) return 1 seed() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/courses.json', 'r' ) as c: cs = json.load(c) for c in cs: if m.Course.query.filter_by(name=c['name']).first() is None: db.session.add(m.Course(name=c['name'])) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/assignments.json', 'r' ) as c: cs = json.load(c) for c in cs: assig = m.Assignment.query.filter_by(name=c['name']).first() if assig is None: db.session.add( m.Assignment( name=c['name'], deadline=datetime.datetime.utcnow() + datetime.timedelta(days=c['deadline']), state=c['state'], description=c['description'], course=m.Course.query.filter_by(name=c['course'] ).first() ) ) else: assig.description = c['description'] assig.state = c['state'] assig.course = m.Course.query.filter_by(name=c['course'] ).first() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/users.json', 'r' ) as c: cs = json.load(c) for c in cs: u = m.User.query.filter_by(name=c['name']).first() courses = { m.Course.query.filter_by(name=name).first(): role for name, role in c['courses'].items() } perms = { course.id: m.CourseRole.query.filter_by(name=name, course_id=course.id).first() for course, name in courses.items() } username = c['name'].split(' ')[0].lower() if u is not None: u.name = c['name'] u.courses = perms u.email = c['name'].replace(' ', '_').lower() + '@example.com' u.password = c['name'] u.username = username u.role = m.Role.query.filter_by(name=c['role']).first() else: u = m.User( name=c['name'], courses=perms, email=c['name'].replace(' ', '_').lower() + '@example.com', password=c['name'], username=username, role=m.Role.query.filter_by(name=c['role']).first() ) db.session.add(u) for course, role in courses.items(): if role == 'Student': for assig in course.assignments: work = m.Work(assignment=assig, user=u) db.session.add( m.File( work=work, name='Top stub dir', is_directory=True ) ) db.session.add(work) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/rubrics.json', 'r' ) as c: cs = json.load(c) for c in cs: for row in c['rows']: assignment = m.Assignment.query.filter_by( name=c['assignment'] ).first() if assignment is not None: rubric_row = m.RubricRow.query.filter_by( header=row['header'], description=row['description'], assignment_id=assignment.id ).first() if rubric_row is None: rubric_row = m.RubricRow( header=row['header'], description=row['description'], assignment=assignment ) db.session.add(rubric_row) for item in row['items']: if not db.session.query( m.RubricItem.query.filter_by( rubricrow_id=rubric_row.id, **item, ).exists() ).scalar(): rubric_item = m.RubricItem( description=item['description'] * 5, header=item['header'], points=item['points'], rubricrow=rubric_row ) db.session.add(rubric_item) db.session.commit()
def register_user() -> JSONResponse[t.Mapping[str, str]]: """Create a new :class:`.models.User`. .. :quickref: User; Create a new user by registering it. :<json str username: The username of the new user. :<json str password: The password of the new user. :<json str email: The email of the new user. :<json str name: The full name of the new user. :>json str access_token: The JWT token that can be used to log in the newly created user. :raises APIException: If the not all given strings are at least 1 char. (INVALID_PARAM) :raises APIException: If there is already a user with the given username. (OBJECT_ALREADY_EXISTS) :raises APIException: If the given email is not a valid email. (INVALID_PARAM) """ content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('username', str), ('password', str), ('email', str), ('name', str)]) username = t.cast(str, content['username']) password = t.cast(str, content['password']) email = t.cast(str, content['email']) name = t.cast(str, content['name']) if not all([username, password, email, name]): raise APIException( 'All fields should contain at least one character', ('The lengths of the given password, username and ' 'email were not all larger than 1'), APICodes.INVALID_PARAM, 400, ) if db.session.query( models.User.query.filter_by(username=username).exists()).scalar(): raise APIException( 'The given username is already in use', f'The username "{username}" is taken', APICodes.OBJECT_ALREADY_EXISTS, 400, ) if not validate_email(email): raise APIException( 'The given email is not valid', f'The email "{email}"', APICodes.INVALID_PARAM, 400, ) role = models.Role.query.filter_by( name=current_app.config['DEFAULT_ROLE']).one() user = models.User(username=username, password=password, email=email, name=name, role=role, active=True) db.session.add(user) db.session.commit() token: str = flask_jwt.create_access_token( identity=user.id, fresh=True, ) return jsonify({'access_token': token})
def test_data(db=None): db = psef.models.db if db is None else db if not app.config['DEBUG']: print('You can not add test data in production mode', file=sys.stderr) return 1 if not os.path.isdir(app.config['UPLOAD_DIR']): os.mkdir(app.config['UPLOAD_DIR']) seed() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/courses.json', 'r') as c: cs = json.load(c) for c in cs: if m.Course.query.filter_by(name=c['name']).first() is None: db.session.add(m.Course.create_and_add(name=c['name'])) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/assignments.json', 'r') as c: cs = json.load(c) for c in cs: assig = m.Assignment.query.filter_by(name=c['name']).first() if assig is None: db.session.add( m.Assignment( name=c['name'], deadline=cg_dt_utils.now() + datetime.timedelta(days=c['deadline']), state=c['state'], description=c['description'], course=m.Course.query.filter_by( name=c['course']).first(), is_lti=False, )) else: assig.description = c['description'] assig.state = c['state'] assig.course = m.Course.query.filter_by( name=c['course']).first() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/users.json', 'r') as c: cs = json.load(c) for c in cs: u = m.User.query.filter_by(name=c['name']).first() courses = { m.Course.query.filter_by(name=name).first(): role for name, role in c['courses'].items() } perms = { course.id: m.CourseRole.query.filter_by(name=name, course_id=course.id).first() for course, name in courses.items() } username = c['name'].split(' ')[0].lower() if u is not None: u.name = c['name'] u.courses = perms u.email = c['name'].replace(' ', '_').lower() + '@example.com' u.password = c['name'] u.username = username u.role = m.Role.query.filter_by(name=c['role']).first() else: u = m.User(name=c['name'], courses=perms, email=c['name'].replace(' ', '_').lower() + '@example.com', password=c['name'], username=username, role=m.Role.query.filter_by(name=c['role']).first()) db.session.add(u) for course, role in courses.items(): if role == 'Student': for assig in course.assignments: work = m.Work(assignment=assig, user=u) f = m.File(work=work, name='Top stub dir ({})'.format(u.name), is_directory=True) filename = str(uuid.uuid4()) shutil.copyfile( __file__, os.path.join(app.config['UPLOAD_DIR'], filename)) f.children = [ m.File(work=work, name='manage.py', is_directory=False, filename=filename) ] db.session.add(f) db.session.add(work) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/rubrics.json', 'r') as c: cs = json.load(c) for c in cs: for row in c['rows']: assignment = m.Assignment.query.filter_by( name=c['assignment']).first() if assignment is not None: rubric_row = m.RubricRow.query.filter_by( header=row['header'], description=row['description'], assignment_id=assignment.id, ).first() if rubric_row is None: rubric_row = m.RubricRow( header=row['header'], description=row['description'], assignment=assignment, rubric_row_type='normal', ) db.session.add(rubric_row) for item in row['items']: if not db.session.query( m.RubricItem.query.filter_by( rubricrow_id=rubric_row.id, **item, ).exists()).scalar(): rubric_item = m.RubricItem( description=item['description'] * 5, header=item['header'], points=item['points'], rubricrow=rubric_row) db.session.add(rubric_item) db.session.commit()
def post_submissions(assignment_id: int) -> EmptyResponse: """Add submissions to the given:class:`.models.Assignment` from a blackboard zip file as :class:`.models.Work` objects. .. :quickref: Assignment; Create works from a blackboard zip. You should upload a file as multiform post request. The key should start with 'file'. Multiple blackboard zips are not supported and result in one zip being chosen at (psuedo) random. :param int assignment_id: The id of the assignment :returns: An empty response with return code 204 :raises APIException: If no assignment with given id exists. (OBJECT_ID_NOT_FOUND) :raises APIException: If there was no file in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the file parameter name is incorrect or if the given file does not contain any valid submissions. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user is not allowed to manage the course attached to the assignment. (INCORRECT_PERMISSION) """ assignment = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('can_upload_bb_zip', assignment.course_id) files = get_submission_files_from_request(check_size=False) try: submissions = psef.files.process_blackboard_zip(files[0]) except Exception: # pylint: disable=broad-except # TODO: Narrow this exception down. submissions = [] if not submissions: raise APIException( "The blackboard zip could not imported or it was empty.", 'The blackboard zip could not' ' be parsed or it did not contain any valid submissions.', APICodes.INVALID_PARAM, 400 ) missing, recalc_missing = assignment.get_divided_amount_missing() sub_lookup = {} for sub in assignment.get_all_latest_submissions(): sub_lookup[sub.user_id] = sub student_course_role = models.CourseRole.query.filter_by( name='Student', course_id=assignment.course_id ).first() global_role = models.Role.query.filter_by(name='Student').first() subs = [] hists = [] found_users = { u.username: u for u in models.User.query.filter( t.cast( models.DbColumn[str], models.User.username, ).in_([si.student_id for si, _ in submissions]) ).options(joinedload(models.User.courses)) } newly_assigned: t.Set[t.Optional[int]] = set() for submission_info, submission_tree in submissions: user = found_users.get(submission_info.student_id, None) if user is None: # TODO: Check if this role still exists user = models.User( name=submission_info.student_name, username=submission_info.student_id, courses={assignment.course_id: student_course_role}, email='', password=None, role=global_role, ) found_users[user.username] = user # We don't need to track the users to insert as we are already # tracking the submissions of them and they are coupled. else: user.courses[assignment.course_id] = student_course_role work = models.Work( assignment=assignment, user=user, created_at=submission_info.created_at, ) subs.append(work) if user.id is not None and user.id in sub_lookup: work.assigned_to = sub_lookup[user.id].assigned_to if work.assigned_to is None: if missing: work.assigned_to = max( missing.keys(), key=lambda k: missing[k] ) missing = recalc_missing(work.assigned_to) sub_lookup[user.id] = work hists.append( work.set_grade( submission_info.grade, current_user, add_to_session=False ) ) work.add_file_tree(db.session, submission_tree) if work.assigned_to is not None: newly_assigned.add(work.assigned_to) assignment.set_graders_to_not_done( list(newly_assigned), send_mail=True, ignore_errors=True, ) db.session.bulk_save_objects(subs) db.session.flush() # TODO: This loop should be eliminated. for hist in hists: hist.work_id = hist.work.id hist.user_id = hist.user.id db.session.bulk_save_objects(hists) db.session.commit() return make_empty_response()
def ensure_lti_user( self) -> t.Tuple[models.User, t.Optional[str], t.Optional[str]]: """Make sure the current LTI user is logged in as a psef user. This is done by first checking if we know a user with the current LTI user_id, if this is the case this is the user we log in and return. Otherwise we check if a user is logged in and this user has no LTI user_id, if this is the case we link the current LTI user_id to the current logged in user and return this user. Otherwise we create a new user and link this user to current LTI user_id. :returns: A tuple containing the items in order: the user found as described above, optionally a new token for the user to login with, optionally the updated email of the user as a string, this is ``None`` if the email was not updated. """ is_logged_in = _user_active() token = None user = None lti_user = models.User.query.filter_by( lti_user_id=self.user_id).first() if is_logged_in and current_user.lti_user_id == self.user_id: # The currently logged in user is now using LTI user = current_user elif lti_user is not None: # LTI users are used before the current logged user. token = flask_jwt.create_access_token( identity=lti_user.id, fresh=True, ) user = lti_user elif is_logged_in and current_user.lti_user_id is None: # TODO show some sort of screen if this linking is wanted current_user.lti_user_id = self.user_id db.session.flush() user = current_user else: # New LTI user id is found and no user is logged in or the current # user has a different LTI user id. A new user is created and # logged in. i = 0 def _get_username() -> str: return self.username + (f' ({i})' if i > 0 else '') while db.session.query( models.User.query.filter_by(username=_get_username()). exists()).scalar(): # pragma: no cover i += 1 user = models.User( lti_user_id=self.user_id, name=self.full_name, email=self.user_email, active=True, password=None, username=_get_username(), ) db.session.add(user) db.session.flush() token = flask_jwt.create_access_token( identity=user.id, fresh=True, ) updated_email = None if user.reset_email_on_lti: user.email = self.user_email updated_email = self.user_email user.reset_email_on_lti = False return user, token, updated_email