def replace_source_tokens(file_path, lookup, out_path): """ Replace tokens in a file and write out the substitution. Args: file_path: the path of the file to be read. lookup: a dictionary containing the substitutions. out_path: file in which the substitutions will be written. """ if not path.isfile(file_path): raise InternalException("File: {} does not exist.".format(file_path)) try: source_file = open(file_path, "r") source = source_file.read() except Exception: raise InternalException("Unable to read from source file.") template = Template(source) output = template.safe_substitute(lookup) try: output_file = open(out_path, "w") output_file.write(output) except Exception: raise InternalException("Unable to write to output file.")
def get_group(gid=None, name=None, owner_tid=None): """ Retrieve a group based on its name or gid. Args: name: the name of the group gid: the gid of the group owner_tid: the tid of the group owner Returns: The group object. """ db = api.common.get_conn() match = {} if name is not None and owner_tid is not None: match.update({"name": name}) match.update({"owner": owner_tid}) elif gid is not None: match.update({"gid": gid}) else: raise InternalException( "Classroom name and owner or gid must be specified to look up a classroom." ) group = db.groups.find_one(match, {"_id": 0}) if group is None: raise InternalException("Could not find that classroom!") return group
def get_user(name=None, uid=None): """ Retrieve a user based on a property. If the user is logged in, it will return that user object. Args: name: the user's username uid: the user's uid Returns: Returns the corresponding user object or None if it could not be found """ db = api.common.get_conn() match = {} if uid is not None: match.update({'uid': uid}) elif name is not None: match.update({'username': name}) elif api.auth.is_logged_in(): match.update({'uid': api.auth.get_uid()}) else: raise InternalException("Uid or name must be specified for get_user") user = db.users.find_one(match) if user is None: raise InternalException("User does not exist") return user
def create_new_team_request(params, uid=None): """ Fulfills new team requests for users who have already registered. Args: team_name: The desired name for the team. Must be unique across users and teams. team_password: The team's password. Returns: True if successful, exception thrown otherwise. """ user = api.user.get_user(uid=uid) if user["teacher"]: raise InternalException("Teachers may not create teams!") validate(new_team_schema, params) current_team = api.team.get_team(tid=user["tid"]) if current_team["team_name"] != user["username"]: raise InternalException( "You can only create one new team per user account!") desired_tid = create_team({ "team_name": params["team_name"], "password": api.common.hash_password(params["team_password"]), "affiliation": current_team["affiliation"], "creator": user["uid"], "country": user["country"], }) return join_team(params["team_name"], params["team_password"], user["uid"])
def assign_shell_account(tid=None): """ Assigns a webshell account for the team. Args: tid: the team id """ db = api.common.get_conn() tid = get_team(tid=tid)["tid"] if db.ssh.find({"tid": tid}).count() > 0: raise InternalException( "Team {} was already assigned a shell account.".format(tid)) if not shell_accounts_available(): raise InternalException("There are no available shell accounts.") db.ssh.update({"tid": { "$exists": False }}, {"$set": { "tid": tid }}, multi=False)
def create_user(username, firstname, lastname, email, password_hash, tid, teacher=False, country="US", admin=False, verified=False): """ This inserts a user directly into the database. It assumes all data is valid. Args: username: user's username firstname: user's first name lastname: user's last name email: user's email password_hash: a hash of the user's password tid: the team id to join teacher: whether this account is a teacher Returns: Returns the uid of the newly created user """ db = api.common.get_conn() settings = api.config.get_settings() uid = api.common.token() if safe_fail(get_user, name=username) is not None: raise InternalException("User already exists!") max_team_size = api.config.get_settings()["max_team_size"] updated_team = db.teams.find_and_modify( query={"tid": tid, "size": {"$lt": max_team_size}}, update={"$inc": {"size": 1}}, new=True) if not updated_team: raise InternalException("There are too many users on this team!") #All teachers are admins. if admin or db.users.count() == 0: admin = True teacher = True user = { 'uid': uid, 'firstname': firstname, 'lastname': lastname, 'username': username, 'email': email, 'password_hash': password_hash, 'tid': tid, 'teacher': teacher, 'admin': admin, 'disabled': False, 'country': country, 'verified': not settings["email"]["email_verification"] or verified, } db.users.insert(user) if settings["email"]["email_verification"] and not user["verified"]: api.email.send_user_verification_email(username) return uid
def get_team(tid=None, name=None): """ Retrieve a team based on a property (tid, name, etc.). Args: tid: team id name: team name Returns: Returns the corresponding team object or None if it could not be found """ db = api.api.common.get_conn() match = {} if tid is not None: match.update({'tid': tid}) elif name is not None: match.update({'team_name': name}) elif api.auth.is_logged_in(): match.update({"tid": api.user.get_user()["tid"]}) else: raise InternalException("Must supply tid or team name to get_team") team = db.teams.find_one(match, {"_id": 0}) if team is None: raise InternalException("Team does not exist.") return team
def switch_role(gid, tid, role): """ Switch a user's given role in his group. Cannot switch to/from owner. """ db = api.common.get_conn() team = api.team.get_team(tid=tid) roles = get_roles_in_group(gid, tid=team["tid"]) if role == "member": if roles["teacher"] and not roles["member"]: db.groups.update({"gid": gid}, {"$pull": {"teachers": tid}, "$push": {"members": tid}}) else: raise InternalException("Team is already a member of that group.") elif role == "teacher": if roles["member"] and not roles["teacher"]: db.groups.update({"gid": gid}, {"$push": {"teachers": tid}, "$pull": {"members": tid}}) else: raise InternalException("Team is already a teacher of that group.") else: raise InternalException("Only supported roles are member and teacher.") for uid in api.team.get_team_uids(tid=team["tid"]): sync_teacher_status(tid, uid)
def get_server(sid=None, name=None): """ Returns the server object corresponding to the sid provided Args: sid: the server id to lookup Returns: The server object """ db = api.common.get_conn() if sid is None: if name is None: raise InternalException("You must specify either an sid or name") else: sid = api.common.hash(name) server = db.shell_servers.find_one({"sid": sid}) if server is None: raise InternalException( "Server with sid '{}' does not exist".format(sid)) return server
def assign_instance_to_team(pid, tid=None, reassign=False): """ Assigns an instance of problem pid to team tid. Updates it in the database. Args: pid: the problem id tid: the team id reassign: whether or not we should assign over an old assignment Returns: The iid that was assigned """ team = api.team.get_team(tid=tid) problem = get_problem(pid=pid) if pid in team["instances"] and not reassign: raise InternalException( "Team with tid {} already has an instance of pid {}.".format( tid, pid)) if len(problem["instances"]) == 0: raise InternalException( "Problem {} has no instances to assign.".format(pid)) instance_number = randint(0, len(problem["instances"]) - 1) iid = problem["instances"][instance_number]["iid"] team["instances"][pid] = iid db = api.common.get_conn() db.teams.update({"tid": tid}, {"$set": team}) return instance_number
def wrapper(*args, **kwds): if 'token' not in session: raise InternalException("CSRF token not in session") if 'token' not in request.form: raise InternalException("CSRF token not in form") if session['token'] != request.form['token']: raise InternalException("CSRF token is not correct") return f(*args, **kwds)
def create_user(username, firstname, lastname, email, password_hash, tid, teacher=False, background="undefined", country="undefined", receive_ctf_emails=False): """ This inserts a user directly into the database. It assumes all data is valid. Args: username: user's username firstname: user's first name lastname: user's last name email: user's email password_hash: a hash of the user's password tid: the team id to join teacher: whether this account is a teacher Returns: Returns the uid of the newly created user """ db = api.common.get_conn() uid = api.common.token() if safe_fail(get_user, name=username) is not None: raise InternalException("User already exists!") updated_team = db.teams.find_and_modify( query={"tid": tid, "size": {"$lt": api.team.max_team_users}}, update={"$inc": {"size": 1}}, new=True) if not updated_team: raise InternalException("There are too many users on this team!") user = { 'uid': uid, 'firstname': firstname, 'lastname': lastname, 'username': username, 'email': email, 'password_hash': password_hash, 'tid': tid, 'teacher': teacher, 'avatar': 3, 'eventid': 0, 'disabled': False, 'level': 'Not Started', 'background': background, 'country': country, 'receive_ctf_emails': receive_ctf_emails } db.users.insert(user) return uid
def get_server_number(sid): """ Gets the server_number designation from sid """ if sid is None: raise InternalException("You must specify a sid") server = get_server(sid=sid) if server is None: raise InternalException( "Server with sid '{}' does not exist".format(sid)) return server.get("server_number")
def assign_instance_to_team(pid, tid=None, reassign=False): """ Assigns an instance of problem pid to team tid. Updates it in the database. Args: pid: the problem id tid: the team id reassign: whether or not we should assign over an old assignment Returns: The iid that was assigned """ team = api.team.get_team(tid=tid) problem = get_problem(pid=pid) available_instances = problem["instances"] settings = api.config.get_settings() if settings["shell_servers"]["enable_sharding"]: available_instances = list( filter( lambda i: i.get("server_number") == team.get( "server_number", 1), problem["instances"])) if pid in team["instances"] and not reassign: raise InternalException( "Team with tid {} already has an instance of pid {}.".format( tid, pid)) if len(available_instances) == 0: if settings["shell_servers"]["enable_sharding"]: raise InternalException( "Your assigned shell server is currently down. Please contact an admin." ) else: raise InternalException( "Problem {} has no instances to assign.".format(pid)) instance_number = randint(0, len(available_instances) - 1) iid = available_instances[instance_number]["iid"] team["instances"][pid] = iid db = api.common.get_conn() db.teams.update({"tid": tid}, {"$set": team}) return instance_number
def clear_submissions(uid=None, tid=None, pid=None): """ Clear submissions for a given team, user, or problems. Args: uid: the user's uid to clear from. tid: the team's tid to clear from. pid: the pid to clear from. """ db = api.common.get_conn() match = {} if pid is not None: match.update({"pid", pid}) elif uid is not None: match.update({"uid": uid}) elif tid is not None: match.update({"tid": tid}) else: raise InternalException("You must supply either a tid, uid, or pid") return db.submissions.remove(match)
def send_user_verification_email(username): """ Emails the user a link to verify his account. If email_verification is enabled in the config then the user won't be able to login until this step is completed. """ settings = api.config.get_settings() db = api.common.get_conn() user = api.user.get_user(name=username) key_query = { "$and": [{ "uid": user["uid"] }, { "email_verification_count": { "$exists": True } }] } previous_key = api.token.find_key(key_query) if previous_key is None: token_value = api.token.set_token( { "uid": user["uid"], "email_verification_count": 1 }, "email_verification") else: if previous_key["email_verification_count"] < settings["email"][ "max_verification_emails"]: token_value = previous_key["tokens"]["email_verification"] db.tokens.find_and_modify( key_query, {"$inc": { "email_verification_count": 1 }}) else: raise InternalException( "User has been sent the maximum number of verification emails." ) #Is there a better way to do this without dragging url_for + app_context into it? verification_link = "{}/api/user/verify?uid={}&token={}".\ format(api.config.competition_urls[0], user["uid"], token_value) body = """ Welcome to {0}! You will need to visit the verification link below to finalize your account's creation. If you believe this to be a mistake, and you haven't recently created an account for {0} then you can safely ignore this email. Verification link: {1} Good luck and have fun! The {0} Team. """.format(api.config.competition_name, verification_link) subject = "{} Account Verification".format(api.config.competition_name) message = Message(body=body, recipients=[user['email']], subject=subject) mail.send(message)
def create_new_team_request(params, uid=None): """ Fulfills new team requests for users who have already registered. Args: team_name: The desired name for the team. Must be unique across users and teams. team_password: The team's password. Returns: True if successful, exception thrown elsewise. """ validate(new_team_schema, params) user = api.user.get_user(uid=uid) current_team = api.team.get_team(tid=user["tid"]) if current_team["team_name"] != user["username"]: raise InternalException( "You can only create one new team per user account!") desired_tid = create_team({ "team_name": params["team_name"], "password": params["team_password"], # The team's affiliation becomes the creator's affiliation. "affiliation": current_team["affiliation"], "eligible": True }) return join_team(params["team_name"], params["team_password"], user["uid"])
def get_achievement(aid=None, name=None, show_disabled=False): """ Gets a single achievement Args: aid: the achievement id name: the name of the achievement show_disabled: Boolean indicating whether or not to show disabled achievements. """ db = api.common.get_conn() match = {} if aid is not None: match.update({'aid': aid}) elif name is not None: match.update({'name': name}) else: raise InternalException("Must supply aid or display name") if not show_disabled: match.update({"disabled": False}) db = api.common.get_conn() achievement = db.achievements.find_one(match, {"_id":0}) if achievement is None: raise SevereInternalException("Could not find achievement! You gave " + str(match)) return achievement
def check_question(question=None, answer=None, data=None): data = join_kwargs(data, question=question, answer=answer) data['question'] = int(data['question']) validate(submission_schema, data) num = data.pop('question') answer = data.pop('answer') question = safe_fail(get_question, num=num) if not question: raise InternalException('Question not found') correct = question['answer'] in answer solved = safe_fail(has_solved, question['qid']) with get_conn() as cursor: uid = session['uid'] points = question['success'] if correct else -question['failure'] query = 'INSERT INTO `submissions` (`uid`, `qid`, `answer`, `points`, `correct`) VALUES (%s, %s, %s, %s, %s);' args = (uid, question['qid'], answer, points if not solved else 0, 1 if correct else 0) cursor.execute(query, args) if correct: return WebSuccess('Correct!', data={'url': get_next_question_url()}) else: return WebError('Incorrect')
def get_assigned_server_number(new_team=True, tid=None): """ Assigns a server number based on current teams count and configured stepping Returns: (int) server_number """ settings = api.config.get_settings()["shell_servers"] db = api.common.get_conn() if new_team: team_count = db.teams.count() else: if not tid: raise InternalException("tid must be specified.") oid = db.teams.find_one({"tid": tid}, {"_id": 1}) if not oid: raise InternalException("Invalid tid.") team_count = db.teams.count({"_id": {"$lt": oid["_id"]}}) assigned_number = 1 steps = settings["steps"] if steps: if team_count < steps[-1]: for i, step in enumerate(steps): if team_count < step: assigned_number = i + 1 break else: assigned_number = 1 + len(steps) + ( team_count - steps[-1]) // settings["default_stepping"] else: assigned_number = team_count // settings["default_stepping"] + 1 if settings["limit_added_range"]: max_number = list( db.shell_servers.find({}, { "server_number": 1 }).sort("server_number", -1).limit(1))[0]["server_number"] return min(max_number, assigned_number) else: return assigned_number
def run_makefile(make_directory): """ Runs a makefile in a given directory. Args: make_directory: directory where the Makefile is located. """ make_path = path.join(make_directory, "Makefile") if not path.isfile(make_path): raise InternalException(make_path + " does not exist.") shell = spur.LocalShell() try: shell.run(["make", "-C", make_directory]) except Exception as e: raise InternalException(str(e))
def get_generator_path(pid): """ Gets a problem generator path. Args: pid: the problem pid Returns: The path to the generator. """ problem = api.problem.get_problem(pid=pid) if not is_autogen_problem(pid): raise InternalException("This problem is not autogenerated.") if not problem.get("generator", False): raise InternalException("Autogenerated problem '{}' does not have a generator.".format(problem["name"])) return path.join(api.problem.grader_base_path, problem["generator"])
def clear_all_submissions(): """ Removes all submissions from the database. """ if DEBUG_KEY is not None: db = api.common.get_conn() db.submissions.remove() api.cache.clear_all() else: raise InternalException("DEBUG Mode must be enabled")
def has_solved(qid, uid=None): if not uid: if 'uid' not in session: raise InternalException('Need uid to see if solved') uid = session['uid'] with get_conn() as cursor: query = 'SELECT 1 FROM `submissions` WHERE `correct` = 1 AND `qid` = %s AND `uid` = %s LIMIT 1;' cursor.execute(query, (qid, uid)) return bool(len(cursor.fetchall()))
def change_group_settings(gid, settings): """ Replace the current settings with the supplied ones. """ db = api.common.get_conn() validate(group_settings_schema, settings) group = api.group.get_group(gid=gid) if group["settings"]["hidden"] and not settings["hidden"]: raise InternalException("You can not change a hidden group back to a public group.") db.groups.update({"gid": group["gid"]}, {"$set": {"settings": settings}})
def get_problem(pid=None, name=None, tid=None, show_disabled=True): """ Gets a single problem. Args: pid: The problem id name: The name of the problem show_disabled: Boolean indicating whether or not to show disabled problems. Defaults to True Returns: The problem dictionary from the database """ db = api.common.get_conn() match = {} if pid is not None: match.update({'pid': pid}) elif name is not None: match.update({'name': name}) else: raise InternalException("Must supply pid or display name") if tid is not None and pid not in get_unlocked_pids(tid, category=None): raise InternalException("You cannot get this problem") if not show_disabled: match.update({"disabled": False}) db = api.common.get_conn() problem = db.problems.find_one(match, {"_id": 0}) if problem is None: raise SevereInternalException("Could not find problem! You gave " + str(match)) return problem
def get_processor(aid): """ Returns the processor module for a given achievement. Args: aid: the achievement id Returns: The processor module """ try: path = get_achievement(aid=aid, show_disabled=True)["processor"] return imp.load_source(path[:-3], join(processor_base_path, path)) except FileNotFoundError: raise InternalException("Achievement processor is offline.")
def verify_user(uid, token_value): """ Verify an unverified user account. Link should have been sent to the user's email. Args: uid: the user id token_value: the verification token value Returns: True if successful verification based on the (uid, token_value) """ db = api.common.get_conn() if uid is None: raise InternalException("You must specify a uid.") token_user = api.token.find_key_by_token("email_verification", token_value) if token_user["uid"] == uid: db.users.find_and_modify({"uid": uid}, {"$set": {"verified": True}}) api.token.delete_token({"uid": uid}, "email_verification") return True else: raise InternalException("This is not a valid token for your user.")
def get_number_of_instances(pid): """ Gets the number of active instances of a problem. Args: pid: the problem id Returns: The number of instances. """ # this is more reliable than before, but it may be a little slow try: return [dirname.isdigit() for dirname in os.listdir(get_instance_path(pid, public=False))].count(True) except FileNotFoundError: raise InternalException("Could not find problem instances.")
def get_grader(pid): """ Returns the grader module for a given problem. Args: pid: the problem id Returns: The grader module """ try: path = get_problem(pid=pid, show_disabled=True)["grader"] return imp.load_source(path[:-3], join(grader_base_path, path)) except FileNotFoundError: raise InternalException("Problem grader for {} is offline.".format(get_problem(pid=pid)['name']))