def put(self, digest, container_id): """ Reset (delete and start) a running DockerContainer instance """ # Containers are mapped to teams user_account = api.user.get_user() tid = user_account['tid'] # fail fast on invalid requests if any(char not in string.hexdigits for char in container_id): raise PicoException("Invalid container ID", 400) if any(char not in string.hexdigits + "sha:" for char in digest): raise PicoException("Invalid image digest", 400) # Delete the container del_result = api.docker.delete(container_id) # Create the container create_result = api.docker.create(tid, digest) if del_result and create_result["success"]: return jsonify({"success": True, "message": "Challenge reset.\nBe sure to use the new port."}) else: return jsonify({"success": False, "message": "Error resetting challenge"})
def post(self): """Create and automatically join new team.""" req = team_req.parse_args(strict=True) curr_user = api.user.get_user() if curr_user['teacher']: raise PicoException('Teachers may not create teams', 403) req['team_name'] = req['team_name'].strip() if not all([ c in string.digits + string.ascii_lowercase + " ()+-,#'&!?" for c in req['team_name'].lower() ]): raise PicoException( "Team names cannot contain special characters other than " + "()+-,#'&!?", status_code=400) if req['team_name'] == curr_user['username']: raise PicoException("Invalid team name", status_code=409) new_tid = api.team.create_and_join_new_team(req['team_name'], req['team_password'], curr_user) res = jsonify({'success': True, 'tid': new_tid}) res.status_code = 201 return res
def post(self, group_id): """ Elevate a specified team within a group to the teacher role. Requires teacher role within the group. """ req = group_modify_team_req.parse_args(strict=True) group = api.group.get_group(group_id) if not group: raise PicoException('Classroom not found', 404) group_teachers = [group['owner']] + group['teachers'] eligible_for_elevation = group['members'] curr_tid = api.user.get_user()['tid'] # Ensure the current user has a teacher role within the group if curr_tid not in group_teachers: raise PicoException( 'You must be a teacher in this classroom to remove a team.', status_code=403 ) # Ensure the specified tid is eligible for elevation if req['team_id'] not in eligible_for_elevation: raise PicoException( 'Team is not eligible for elevation to teacher role', status_code=422 ) api.group.elevate_team(group_id, req['team_id']) return jsonify({ 'success': True })
def get(self, group_id): """Retrieve a scoreboard page for a group.""" group = api.group.get_group(gid=group_id) if not group: raise PicoException('Classroom not found', 404) group_members = [group['owner']] + group['members'] + group['teachers'] curr_user = api.user.get_user() if (not curr_user or (curr_user['tid'] not in group_members and not curr_user['admin'])): raise PicoException("You do not have permission to " + "view this classroom's scoreboard.", 403) req = scoreboard_page_req.parse_args(strict=True) if req['search'] is not None: page = api.stats.get_filtered_scoreboard_page( {'group_id': group_id}, req['search'], req['page'] or 1 ) else: page = api.stats.get_scoreboard_page( {'group_id': group_id}, req['page']) return jsonify({ 'scoreboard': page[0], 'current_page': page[1], 'total_pages': page[2] })
def post(self, group_id): """ Remove a specified team from a group. Requires teacher role within the group. """ req = group_remove_team_req.parse_args(strict=True) group = api.group.get_group(group_id) if not group: raise PicoException('Group not found', 404) group_teachers = [group['owner']] + group['teachers'] eligible_for_removal = group['members'] + group['teachers'] curr_tid = api.user.get_user()['tid'] # Ensure the user has a teacher role within the group if curr_tid not in group_teachers: raise PicoException( 'You must be a teacher in this group to remove a team.', status_code=403) # Ensure the specified tid is a member of the group if req['team_id'] not in eligible_for_removal: raise PicoException( 'Specified team is not eligible for removal from this group', status_code=422) api.group.leave_group(group_id, req['team_id']) return jsonify({'success': True})
def get(self, problem_id): """Retrieve a specific problem.""" # Ensure that the problem exists problem = api.problem.get_problem(problem_id) if not problem: raise PicoException("Problem not found", status_code=404) # Add synthetic fields curr_user = api.user.get_user() problem["solves"] = api.stats.get_problem_solves(problem["pid"]) problem["unlocked"] = problem["pid"] in api.problem.get_unlocked_pids( curr_user["tid"]) problem["solved"] = problem["pid"] in api.problem.get_solved_pids( tid=curr_user["tid"]) if curr_user.get("admin", False): problem["reviews"] = api.problem_feedback.get_problem_feedback( pid=problem["pid"], count_only=True) # Ensure that the user has unlocked it if not problem["unlocked"]: raise PicoException("You have not unlocked this problem", 403) # Strip out instance and system info if not admin curr_user = api.user.get_user() if not curr_user.get("admin", False): problem = api.problem.filter_problem_instances( problem, curr_user["tid"]) problem = api.problem.sanitize_problem_data(problem) return jsonify(problem)
def set_htb_id(user_name, htb_id): """ Note: This could be done with def update_extdata, however update_extdata looks up the logged in user. This request is being performed by a challenge at the request of a user, so the user lookup there would fail. Associate a users Hack the Box ID with their User data. Args: user_name: the user who's profile is updated htb_id: the HTB id for the user """ user = get_user(name=user_name) if not user: raise PicoException("Could not find user", 400) if "htb_id" in user: raise PicoException("HTB ID already set", 409) users = get_all_users() for u in users: if "htb_id" in u: if htb_id == u["htb_id"]: raise PicoException("Another User has this HTB ID already", 409) db = api.db.get_conn() db.users.update_one({"uid": user["uid"]}, {"$set": {"htb_id": htb_id}}) return {"success": True}
def get(self, problem_id): """Retrieve a specific problem.""" # Ensure that the problem exists problem = api.problem.get_problem(problem_id) if not problem: raise PicoException('Problem not found', status_code=404) # Add synthetic fields curr_user = api.user.get_user() problem['solves'] = api.stats.get_problem_solves(problem['pid']) problem['unlocked'] = \ problem['pid'] in api.problem.get_unlocked_pids( curr_user['tid']) problem['solved'] = \ problem['pid'] in api.problem.get_solved_pids(curr_user['tid']) if curr_user.get('admin', False): problem['reviews'] = api.problem_feedback.get_problem_feedback( pid=problem['pid']) # Ensure that the user has unlocked it if not problem['unlocked']: raise PicoException('You have not unlocked this problem', 403) # Strip out instance and system info if not admin curr_user = api.user.get_user() if not curr_user.get('admin', False): problem = api.problem.filter_problem_instances( problem, curr_user['tid']) problem = api.problem.sanitize_problem_data(problem) return jsonify(problem)
def get(self, group_id): """Retrieve a scoreboard page for a group.""" group = api.group.get_group(gid=group_id) if not group: raise PicoException("Classroom not found", 404) group_members = [group["owner"]] + group["members"] + group["teachers"] curr_user = api.user.get_user() if not curr_user or (curr_user["tid"] not in group_members and not curr_user["admin"]): raise PicoException( "You do not have permission to " + "view this classroom's scoreboard.", 403, ) req = scoreboard_page_req.parse_args(strict=True) if req["search"] is not None: page = api.stats.get_filtered_scoreboard_page( {"group_id": group_id}, req["search"], req["page"] or 1) else: page = api.stats.get_scoreboard_page({"group_id": group_id}, req["page"]) return jsonify({ "scoreboard": page[0], "current_page": page[1], "total_pages": page[2] })
def post(self, group_id): """ Remove a specified team from a group. Requires teacher role within the group. """ req = group_modify_team_req.parse_args(strict=True) group = api.group.get_group(group_id) if not group: raise PicoException("Classroom not found", 404) group_teachers = [group["owner"]] + group["teachers"] eligible_for_removal = group["members"] + group["teachers"] curr_tid = api.user.get_user()["tid"] # Ensure the user has a teacher role within the group if curr_tid not in group_teachers: raise PicoException( "You must be a teacher in this classroom to remove a team.", status_code=403, ) # Ensure the specified tid is a member of the group if req["team_id"] not in eligible_for_removal: raise PicoException( "Team is not eligible for removal from this classroom", status_code=422) api.group.leave_group(group_id, req["team_id"]) return jsonify({"success": True})
def update_password_request(params): """ Update team password. Assumes args are keys in params. Args: params: new-password: the new password new-password-confirmation: confirmation of password """ user = api.user.get_user() current_team = api.team.get_team(tid=user["tid"]) if current_team["team_name"] == user["username"]: raise PicoException("You have not created a team yet.", 422) if params["new-password"] != params["new-password-confirmation"]: raise PicoException("Your team passwords do not match.", 422) db = api.db.get_conn() db.teams.update({'tid': user['tid']}, { '$set': { 'password': api.common.hash_password(params["new-password"]) } })
def update_password_request(params, uid=None, check_current=False): """ Update account password. Assumes args are keys in params. Args: uid: uid to reset check_current: whether to ensure that current-password is correct params (dict): current-password: the users current password new-password: the new password new-password-confirmation: confirmation of password """ user = get_user(uid=uid, include_pw_hash=True) if check_current and not api.user.confirm_password( params["current-password"], user['password_hash']): raise PicoException("Your current password is incorrect.", 422) if params["new-password"] != params["new-password-confirmation"]: raise PicoException("Your passwords do not match.", 422) db = api.db.get_conn() db.users.update({'uid': user['uid']}, { '$set': { 'password_hash': api.common.hash_password(params["new-password"]) } })
def remove_member(tid, uid): """ Move the specified member back to their self-team. Eliminates custom team if no members remain. The member specified cannot have submitted any valid solutions. """ team = get_team(tid) curr_user_uid = api.user.get_user()["uid"] curr_user_is_creator = curr_user_uid == team.get("creator") if not curr_user_is_creator and uid != curr_user_uid: raise PicoException("Only the team captain can kick other members.", status_code=403) if uid not in get_team_uids(tid): raise PicoException("Specified user is not a member of this team.", status_code=404) if api.user.get_user(uid=uid)["username"] == team["team_name"]: raise PicoException("Cannot remove self from default team", status_code=403) if not api.user.can_leave_team(uid): if curr_user_is_creator and curr_user_uid == uid: raise PicoException( "Team captain must be the only remaining member in order " + "to leave.", status_code=403, ) else: raise PicoException( "This team member has submitted a flag and can no longer " + "be removed.", status_code=403, ) self_team_tid = api.team.get_team(name=api.user.get_user( uid=uid)["username"])["tid"] db = api.db.get_conn() db.users.find_one_and_update({"uid": uid}, {"$set": { "tid": self_team_tid }}) db.teams.find_one_and_update({"tid": self_team_tid}, {"$inc": {"size": 1}}) db.teams.find_one_and_update({"tid": tid}, {"$inc": {"size": -1}}) # Delete the custom team if no members remain remaining_team_size = db.teams.find_one({"tid": tid}, {"size": 1})["size"] if remaining_team_size < 1: delete_team(tid) # Copy any acquired group memberships back to the self-team for group in get_groups(tid): api.group.join_group(gid=group["gid"], tid=self_team_tid)
def create_and_join_new_team(team_name, team_password, user): """ Fulfill new team requests for users who have already registered. Seperate from create_team() as we need to do additional logic: - Check that the new team name doesn't conflict with a team or user - Check that the user creating the team is on their initial 1-person "username team" Args: team_name: The desired name for the team. Must be unique across users and teams. team_password: The team's password. user: The user Returns: The tid of the new team Raises: PicoException if a team or user with name team_name already exists, or if user has already created a team """ # Ensure name does not conflict with existing user or team db = api.db.get_conn() if db.users.find_one( {"username": team_name}, collation=Collation(locale="en", strength=CollationStrength.PRIMARY), ): raise PicoException("There is already a user with this name.", 409) if db.teams.find_one( {"team_name": team_name}, collation=Collation(locale="en", strength=CollationStrength.PRIMARY), ): raise PicoException("There is already a team with this name.", 409) # Make sure the creating user has not already created a team current_team = api.team.get_team(tid=user["tid"]) if current_team["team_name"] != user["username"]: raise PicoException( "You can only create one new team per user account!", 422) # Create the team and join it new_tid = create_team({ "team_name": team_name, "password": api.common.hash_password(team_password), "affiliation": current_team["affiliation"], "creator": user["uid"], "allow_ineligible_members": False, }) join_team(team_name, team_password, user) return new_tid
def wrapper(*args, **kwds): if 'token' not in session: raise PicoException( 'Internal server error', data={'debug': 'CSRF token not found in session'}) submitted_token = request.headers.get('X-CSRF-Token', None) if submitted_token is None: raise PicoException('CSRF token not included in request', 403) if session['token'] != submitted_token: raise PicoException('CSRF token is not correct', 403) return f(*args, **kwds)
def wrapper(*args, **kwds): if "token" not in session: raise PicoException( "Internal server error", data={"debug": "CSRF token not found in session"}, ) submitted_token = request.headers.get("X-CSRF-Token", None) if submitted_token is None: raise PicoException("CSRF token not included in request", 403) if session["token"] != submitted_token: raise PicoException("CSRF token is not correct", 403) return f(*args, **kwds)
def get(self, scoreboard_id): """Get a list of teams' score progressions.""" req = score_progressions_req.parse_args(strict=True) scoreboard = api.scoreboards.get_scoreboard(scoreboard_id) if not scoreboard: raise PicoException('Scoreboard not found', 404) if req['limit'] and (not api.user.is_logged_in() or not api.user.get_user()['admin']): raise PicoException('Must be admin to specify limit', 403) return jsonify( api.stats.get_top_teams_score_progressions( limit=(req['limit'] or 5), scoreboard_id=scoreboard_id))
def get(self, problem_id): """Get the walkthrough for a problem, if unlocked.""" uid = api.user.get_user()['uid'] problem = api.problem.get_problem(problem_id) if problem is None: raise PicoException('Problem not found', 404) if problem.get('walkthrough', None) is None: raise PicoException('This problem does not have a walkthrough!', status_code=404) if problem['pid'] not in api.problem.get_unlocked_walkthroughs(uid): raise PicoException("You haven't unlocked this walkthrough yet!", status_code=403) return jsonify({'walkthrough': problem['walkthrough']})
def delete(self, group_id): """Delete a group. Must be the owner of the group.""" group = api.group.get_group(gid=group_id) if not group: raise PicoException('Group not found', 404) curr_user = api.user.get_user() if (curr_user['tid'] != group['owner'] and not curr_user['admin']): raise PicoException( 'You do not have permission to delete this group.', 403) api.group.delete_group(group_id) return jsonify({'success': True})
def get_assigned_server_number(new_team=True, tid=None): """ Assign a server number based on current team count and configured stepping. Returns: (int) server_number """ settings = api.config.get_settings()["shell_servers"] db = api.db.get_conn() if new_team: team_count = db.teams.count() else: if not tid: raise PicoException("tid must be specified.") oid = db.teams.find_one({"tid": tid}, {"_id": 1}) if not oid: raise PicoException("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 get(self, group_id): """Remove your own team from this group.""" group = api.group.get_group(group_id) if not group: raise PicoException("Classroom not found", 404) eligible_for_removal = group["members"] + group["teachers"] curr_tid = api.user.get_user()["tid"] if curr_tid not in eligible_for_removal: raise PicoException( "Team is not eligible for removal from this classroom", status_code=422) api.group.leave_group(group_id, curr_tid) return jsonify({"success": True})
def get(self, group_id): """Get flag sharing statistics for a specific group.""" group = api.group.get_group(gid=group_id) if not group: raise PicoException('Group not found', 404) curr_user = api.user.get_user() if (curr_user['tid'] not in (group['teachers'] + [group['owner']]) and not curr_user['admin']): raise PicoException( 'You do not have permission to view these statistics.', 403) return jsonify( api.stats.check_invalid_instance_submissions(group['gid']))
def get(self, group_id): """Remove your own team from this group.""" group = api.group.get_group(group_id) if not group: raise PicoException('Group not found', 404) eligible_for_removal = group['members'] + group['teachers'] curr_tid = api.user.get_user()['tid'] if curr_tid not in eligible_for_removal: raise PicoException( 'Specified team is not eligible for removal from this group', status_code=422) api.group.leave_group(group_id, curr_tid) return jsonify({'success': True})
def post(self): """Join a team by providing its name and password.""" current_user = api.user.get_user() if current_user['teacher']: raise PicoException('Teachers may not join teams!', 403) req = team_change_req.parse_args(strict=True) # Ensure that the team exists team = api.team.get_team(name=req['team_name']) if team is None: raise PicoException('Team not found', 404) api.team.join_team(req['team_name'], req['team_password'], current_user) return jsonify({'success': True})
def assign_instance_to_team(pid, tid=None, reassign=False): """ Assign an instance of problem pid to team tid. 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) 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 PicoException( "Team with tid {} already has an instance of pid {}.".format( tid, pid)) if len(available_instances) == 0: if settings["shell_servers"]["enable_sharding"]: raise PicoException( "Your assigned shell server is currently down. " + "Please contact an admin.") else: raise PicoException( "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.db.get_conn() db.teams.update({"tid": tid}, {"$set": team}) return instance_number
def request_password_reset(username): """ Email a user a link to reset their password. Args: username: the username of the account Raises: PicoException: if provided username not found """ refresh_email_settings() user = api.user.get_user(name=username) if user is None: raise PicoException('Username not found', 404) token_value = api.token.set_token({"uid": user['uid']}, "password_reset") settings = api.config.get_settings() body = settings["email"]["reset_password_body"].format( # noqa:E501 competition_name=settings["competition_name"], competition_url=settings["competition_url"], username=username, token_value=token_value) subject = "{} Password Reset".format(settings["competition_name"]) message = Message(body=body, recipients=[user['email']], subject=subject) mail.send(message)
def post(self, group_id): """Send an email invite to join this team.""" req = group_invite_req.parse_args(strict=True) group = api.group.get_group(gid=group_id) if not group: raise PicoException('Group not found', 404) curr_user = api.user.get_user() if (curr_user['tid'] not in (group['teachers'] + [group['owner']]) and not curr_user['admin']): raise PicoException( 'You do not have permission to invite members to this group.', status_code=403) api.email.send_email_invite(group_id, req['email'], req['as_teacher']) return jsonify({'success': True})
def patch(self, group_id): """Modify a group's settings (other fields are not available).""" req = group_patch_req.parse_args(strict=True) group = api.group.get_group(gid=group_id) if not group: raise PicoException('Group not found', 404) curr_user = api.user.get_user() if (curr_user['tid'] not in ([group['owner']] + group['teachers']) and not curr_user['admin']): raise PicoException( 'You do not have permission to modify this group.', 403) api.group.change_group_settings(group_id, req['settings']) return jsonify({'success': True})
def get(self, problem_id): """Get the walkthrough for a problem, if unlocked.""" uid = api.user.get_user()["uid"] problem = api.problem.get_problem(problem_id, { "pid": 1, "walkthrough": 1 }) if problem is None: raise PicoException("Problem not found", 404) if problem.get("walkthrough", None) is None: raise PicoException("This problem does not have a walkthrough!", status_code=404) if problem["pid"] not in api.problem.get_unlocked_walkthroughs(uid): raise PicoException("You haven't unlocked this walkthrough yet!", status_code=403) return jsonify({"walkthrough": problem["walkthrough"]})
def get(self, team_id): """ Re-evaluate a team's scoreboard eligibilities. May be useful if a former member who had previously caused their team to become ineligible for a scoreboard deletes their account, or if a new scoreboard is added after the team's creation. """ team = api.team.get_team(team_id) if not team: raise PicoException('Team not found', 404) team_members = api.team.get_team_members(tid=team_id, show_disabled=False) all_scoreboards = api.scoreboards.get_all_scoreboards() member_eligibilities = dict() for member in team_members: member_eligibilities[member['uid']] = { scoreboard['sid'] for scoreboard in all_scoreboards if api.scoreboards.is_eligible(member, scoreboard) } team_eligibilities = list( set.intersection(*member_eligibilities.values())) db = api.db.get_conn() db.teams.find_one_and_update( {"tid": team_id}, {"$set": { "eligibilities": team_eligibilities }}) return jsonify({'success': True, 'eligibilities': team_eligibilities})