From a3a7d75ae8f0fb7a4a3e9fb50b112a05a3f0d89f Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Wed, 22 Mar 2017 20:00:45 -0400 Subject: [PATCH] Plugins enhanced utils (#231) * Updating utils functions to be monkey patchable * Also fixing a team email update issue * Adding more tests --- CTFd/__init__.py | 15 +++--- CTFd/admin/__init__.py | 88 ++++++++++++++++----------------- CTFd/admin/challenges.py | 13 ++--- CTFd/admin/containers.py | 24 ++++----- CTFd/admin/keys.py | 6 +-- CTFd/admin/pages.py | 10 ++-- CTFd/admin/scoreboard.py | 6 +-- CTFd/admin/statistics.py | 6 +-- CTFd/admin/teams.py | 15 +++--- CTFd/auth.py | 28 +++++------ CTFd/challenges.py | 81 +++++++++++++++--------------- CTFd/scoreboard.py | 19 +++---- CTFd/templates/admin/teams.html | 6 ++- CTFd/views.py | 74 +++++++++++++-------------- tests/helpers.py | 62 ++++++++++++++++++++++- tests/test_admin_facing.py | 76 ++++++++++++++++++++++++++++ tests/test_user_facing.py | 17 ++++++- 17 files changed, 351 insertions(+), 195 deletions(-) diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 93e11445..2fc2a748 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -8,7 +8,8 @@ from sqlalchemy.exc import OperationalError, ProgrammingError from sqlalchemy_utils import database_exists, create_database from six.moves import input -from CTFd.utils import get_config, set_config, cache, migrate, migrate_upgrade, migrate_stamp +from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp +from CTFd import utils __version__ = '1.0.1' @@ -16,7 +17,7 @@ class ThemeLoader(FileSystemLoader): def get_source(self, environment, template): if template.startswith('admin/'): return super(ThemeLoader, self).get_source(environment, template) - theme = get_config('ctf_theme') + theme = utils.get_config('ctf_theme') template = "/".join([theme, template]) return super(ThemeLoader, self).get_source(environment, template) @@ -53,10 +54,10 @@ def create_app(config='CTFd.config.Config'): cache.init_app(app) app.cache = cache - version = get_config('ctf_version') + version = utils.get_config('ctf_version') if not version: ## Upgrading from an unversioned CTFd - set_config('ctf_version', __version__) + utils.set_config('ctf_version', __version__) if version and (StrictVersion(version) < StrictVersion(__version__)): ## Upgrading from an older version of CTFd print("/*\\ CTFd has updated and must update the database! /*\\") @@ -65,13 +66,13 @@ def create_app(config='CTFd.config.Config'): if input('Run database migrations (Y/N)').lower().strip() == 'y': migrate_stamp() migrate_upgrade() - set_config('ctf_version', __version__) + utils.set_config('ctf_version', __version__) else: print('/*\\ Ignored database migrations... /*\\') exit() - if not get_config('ctf_theme'): - set_config('ctf_theme', 'original') + if not utils.get_config('ctf_theme'): + utils.set_config('ctf_theme', 'original') from CTFd.views import views from CTFd.challenges import challenges diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index 8a58ff76..d1056088 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -7,9 +7,7 @@ from flask import current_app as app, render_template, request, redirect, jsonif from passlib.hash import bcrypt_sha256 from sqlalchemy.sql import not_ -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file, get_configurable_plugins +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.scoreboard import get_standings from CTFd.plugins.keys import get_key_class, KEY_CLASSES @@ -22,6 +20,8 @@ from CTFd.admin.containers import admin_containers from CTFd.admin.keys import admin_keys from CTFd.admin.teams import admin_teams +from CTFd import utils + admin = Blueprint('admin', __name__) @@ -38,13 +38,13 @@ def admin_view(): @admins_only def admin_plugin_config(plugin): if request.method == 'GET': - if plugin in get_configurable_plugins(): + if plugin in utils.get_configurable_plugins(): config = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read() return render_template('admin/page.html', content=config) abort(404) elif request.method == 'POST': for k, v in request.form.items(): - set_config(k, v) + utils.set_config(k, v) return '1' @@ -80,28 +80,28 @@ def admin_config(): mail_tls = None mail_ssl = None finally: - view_challenges_unregistered = set_config('view_challenges_unregistered', view_challenges_unregistered) - view_scoreboard_if_authed = set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) - hide_scores = set_config('hide_scores', hide_scores) - prevent_registration = set_config('prevent_registration', prevent_registration) - prevent_name_change = set_config('prevent_name_change', prevent_name_change) - view_after_ctf = set_config('view_after_ctf', view_after_ctf) - verify_emails = set_config('verify_emails', verify_emails) - mail_tls = set_config('mail_tls', mail_tls) - mail_ssl = set_config('mail_ssl', mail_ssl) + view_challenges_unregistered = utils.set_config('view_challenges_unregistered', view_challenges_unregistered) + view_scoreboard_if_authed = utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) + hide_scores = utils.set_config('hide_scores', hide_scores) + prevent_registration = utils.set_config('prevent_registration', prevent_registration) + prevent_name_change = utils.set_config('prevent_name_change', prevent_name_change) + view_after_ctf = utils.set_config('view_after_ctf', view_after_ctf) + verify_emails = utils.set_config('verify_emails', verify_emails) + mail_tls = utils.set_config('mail_tls', mail_tls) + mail_ssl = utils.set_config('mail_ssl', mail_ssl) - mail_server = set_config("mail_server", request.form.get('mail_server', None)) - mail_port = set_config("mail_port", request.form.get('mail_port', None)) + mail_server = utils.set_config("mail_server", request.form.get('mail_server', None)) + mail_port = utils.set_config("mail_port", request.form.get('mail_port', None)) - mail_username = set_config("mail_username", request.form.get('mail_username', None)) - mail_password = set_config("mail_password", request.form.get('mail_password', None)) + mail_username = utils.set_config("mail_username", request.form.get('mail_username', None)) + mail_password = utils.set_config("mail_password", request.form.get('mail_password', None)) - ctf_name = set_config("ctf_name", request.form.get('ctf_name', None)) - ctf_theme = set_config("ctf_theme", request.form.get('ctf_theme', None)) + ctf_name = utils.set_config("ctf_name", request.form.get('ctf_name', None)) + ctf_theme = utils.set_config("ctf_theme", request.form.get('ctf_theme', None)) - mailfrom_addr = set_config("mailfrom_addr", request.form.get('mailfrom_addr', None)) - mg_base_url = set_config("mg_base_url", request.form.get('mg_base_url', None)) - mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) + mailfrom_addr = utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None)) + mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None)) + mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None)) db_start = Config.query.filter_by(key='start').first() db_start.value = start @@ -120,36 +120,36 @@ def admin_config(): with app.app_context(): cache.clear() - ctf_name = get_config('ctf_name') - ctf_theme = get_config('ctf_theme') - hide_scores = get_config('hide_scores') + ctf_name = utils.get_config('ctf_name') + ctf_theme = utils.get_config('ctf_theme') + hide_scores = utils.get_config('hide_scores') - mail_server = get_config('mail_server') - mail_port = get_config('mail_port') - mail_username = get_config('mail_username') - mail_password = get_config('mail_password') + mail_server = utils.get_config('mail_server') + mail_port = utils.get_config('mail_port') + mail_username = utils.get_config('mail_username') + mail_password = utils.get_config('mail_password') - mailfrom_addr = get_config('mailfrom_addr') - mg_api_key = get_config('mg_api_key') - mg_base_url = get_config('mg_base_url') + mailfrom_addr = utils.get_config('mailfrom_addr') + mg_api_key = utils.get_config('mg_api_key') + mg_base_url = utils.get_config('mg_base_url') - view_after_ctf = get_config('view_after_ctf') - start = get_config('start') - end = get_config('end') + view_after_ctf = utils.get_config('view_after_ctf') + start = utils.get_config('start') + end = utils.get_config('end') - mail_tls = get_config('mail_tls') - mail_ssl = get_config('mail_ssl') + mail_tls = utils.get_config('mail_tls') + mail_ssl = utils.get_config('mail_ssl') - view_challenges_unregistered = get_config('view_challenges_unregistered') - view_scoreboard_if_authed = get_config('view_scoreboard_if_authed') - prevent_registration = get_config('prevent_registration') - prevent_name_change = get_config('prevent_name_change') - verify_emails = get_config('verify_emails') + view_challenges_unregistered = utils.get_config('view_challenges_unregistered') + view_scoreboard_if_authed = utils.get_config('view_scoreboard_if_authed') + prevent_registration = utils.get_config('prevent_registration') + prevent_name_change = utils.get_config('prevent_name_change') + verify_emails = utils.get_config('verify_emails') db.session.commit() db.session.close() - themes = get_themes() + themes = utils.get_themes() themes.remove(ctf_theme) return render_template('admin/config.html', diff --git a/CTFd/admin/challenges.py b/CTFd/admin/challenges.py index 98dc0abc..c3fe0333 100644 --- a/CTFd/admin/challenges.py +++ b/CTFd/admin/challenges.py @@ -1,10 +1,11 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES + +from CTFd import utils + import os admin_challenges = Blueprint('admin_challenges', __name__) @@ -109,7 +110,7 @@ def admin_files(chalid): files = request.files.getlist('files[]') for f in files: - upload_file(file=f, chalid=chalid) + utils.upload_file(file=f, chalid=chalid) db.session.commit() db.session.close() @@ -162,7 +163,7 @@ def admin_create_chal(): db.session.commit() for f in files: - upload_file(file=f, chalid=chal.id) + utils.upload_file(file=f, chalid=chal.id) db.session.commit() db.session.close() @@ -183,7 +184,7 @@ def admin_delete_chal(): for file in files: upload_folder = app.config['UPLOAD_FOLDER'] folder = os.path.dirname(os.path.join(os.path.normpath(app.root_path), upload_folder, file.location)) - rmdir(folder) + utils.rmdir(folder) Tags.query.filter_by(chal=challenge.id).delete() Challenges.query.filter_by(id=challenge.id).delete() db.session.commit() diff --git a/CTFd/admin/containers.py b/CTFd/admin/containers.py index 4ba80cba..2c8762ae 100644 --- a/CTFd/admin/containers.py +++ b/CTFd/admin/containers.py @@ -1,9 +1,9 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError +from CTFd import utils + admin_containers = Blueprint('admin_containers', __name__) @admin_containers.route('/admin/containers', methods=['GET']) @@ -11,8 +11,8 @@ admin_containers = Blueprint('admin_containers', __name__) def list_container(): containers = Containers.query.all() for c in containers: - c.status = container_status(c.name) - c.ports = ', '.join(container_ports(c.name, verbose=True)) + c.status = utils.container_status(c.name) + c.ports = ', '.join(utils.container_ports(c.name, verbose=True)) return render_template('admin/containers.html', containers=containers) @@ -20,7 +20,7 @@ def list_container(): @admins_only def stop_container(container_id): container = Containers.query.filter_by(id=container_id).first_or_404() - if container_stop(container.name): + if utils.container_stop(container.name): return '1' else: return '0' @@ -30,13 +30,13 @@ def stop_container(container_id): @admins_only def run_container(container_id): container = Containers.query.filter_by(id=container_id).first_or_404() - if container_status(container.name) == 'missing': - if run_image(container.name): + if utils.container_status(container.name) == 'missing': + if utils.run_image(container.name): return '1' else: return '0' else: - if container_start(container.name): + if utils.container_start(container.name): return '1' else: return '0' @@ -46,7 +46,7 @@ def run_container(container_id): @admins_only def delete_container(container_id): container = Containers.query.filter_by(id=container_id).first_or_404() - if delete_image(container.name): + if utils.delete_image(container.name): db.session.delete(container) db.session.commit() db.session.close() @@ -61,6 +61,6 @@ def new_container(): return redirect(url_for('admin_containers.list_container')) buildfile = request.form.get('buildfile') files = request.files.getlist('files[]') - create_image(name=name, buildfile=buildfile, files=files) - run_image(name) + utils.create_image(name=name, buildfile=buildfile, files=files) + utils.run_image(name) return redirect(url_for('admin_containers.list_container')) \ No newline at end of file diff --git a/CTFd/admin/keys.py b/CTFd/admin/keys.py index 7ea64cf1..f93aebb5 100644 --- a/CTFd/admin/keys.py +++ b/CTFd/admin/keys.py @@ -1,10 +1,10 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.plugins.keys import get_key_class, KEY_CLASSES +from CTFd import utils + admin_keys = Blueprint('admin_keys', __name__) @admin_keys.route('/admin/key_types', methods=['GET']) diff --git a/CTFd/admin/pages.py b/CTFd/admin/pages.py index ef17d89a..a397a58a 100644 --- a/CTFd/admin/pages.py +++ b/CTFd/admin/pages.py @@ -1,9 +1,9 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError +from CTFd import utils + admin_pages = Blueprint('admin_pages', __name__) @admin_pages.route('/admin/css', methods=['GET', 'POST']) @@ -11,7 +11,7 @@ admin_pages = Blueprint('admin_pages', __name__) def admin_css(): if request.method == 'POST': css = request.form['css'] - css = set_config('css', css) + css = utils.set_config('css', css) with app.app_context(): cache.clear() return '1' @@ -49,7 +49,7 @@ def admin_pages_view(route): db.session.close() return redirect(url_for('admin_pages.admin_pages_view')) pages = Pages.query.all() - return render_template('admin/pages.html', routes=pages, css=get_config('css')) + return render_template('admin/pages.html', routes=pages, css=utils.get_config('css')) @admin_pages.route('/admin/page//delete', methods=['POST']) diff --git a/CTFd/admin/scoreboard.py b/CTFd/admin/scoreboard.py index deb4ddb1..3a3491ed 100644 --- a/CTFd/admin/scoreboard.py +++ b/CTFd/admin/scoreboard.py @@ -1,10 +1,10 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.scoreboard import get_standings +from CTFd import utils + admin_scoreboard = Blueprint('admin_scoreboard', __name__) @admin_scoreboard.route('/admin/scoreboard') diff --git a/CTFd/admin/statistics.py b/CTFd/admin/statistics.py index 8e24414f..e46b1335 100644 --- a/CTFd/admin/statistics.py +++ b/CTFd/admin/statistics.py @@ -1,9 +1,9 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError +from CTFd import utils + admin_statistics = Blueprint('admin_statistics', __name__) @admin_statistics.route('/admin/graphs') diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index a008369c..d8d58d93 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -1,11 +1,11 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ - set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache, upload_file +from CTFd.utils import admins_only, is_admin, cache from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from passlib.hash import bcrypt_sha256 from sqlalchemy.sql import not_ +from CTFd import utils + admin_teams = Blueprint('admin_teams', __name__) @admin_teams.route('/admin/teams', defaults={'page': '1'}) @@ -84,7 +84,8 @@ def admin_team(teamid): return jsonify({'data': errors}) else: user.name = name - user.email = email + if email: + user.email = email if password: user.password = bcrypt_sha256.encrypt(password) user.website = website @@ -101,7 +102,7 @@ def email_user(teamid): message = request.form.get('msg', None) team = Teams.query.filter(Teams.id == teamid).first() if message and team: - if sendmail(team.email, message): + if utils.sendmail(team.email, message): return '1' return '0' @@ -160,7 +161,7 @@ def admin_solves(teamid="all"): 'team': x.teamid, 'value': x.chal.value, 'category': x.chal.category, - 'time': unix_time(x.date) + 'time': utils.unix_time(x.date) }) for award in awards: json_data['solves'].append({ @@ -169,7 +170,7 @@ def admin_solves(teamid="all"): 'team': award.teamid, 'value': award.value, 'category': award.category or "Award", - 'time': unix_time(award.date) + 'time': utils.unix_time(award.date) }) json_data['solves'].sort(key=lambda k: k['time']) return jsonify(json_data) diff --git a/CTFd/auth.py b/CTFd/auth.py index 7cd9c3cc..0c5576b6 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -8,8 +8,8 @@ from flask import current_app as app, render_template, request, redirect, url_fo from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature from passlib.hash import bcrypt_sha256 -from CTFd.utils import sha512, is_safe_url, authed, can_send_mail, sendmail, can_register, get_config, verify_email from CTFd.models import db, Teams +from CTFd import utils auth = Blueprint('auth', __name__) @@ -17,7 +17,7 @@ auth = Blueprint('auth', __name__) @auth.route('/confirm', methods=['POST', 'GET']) @auth.route('/confirm/', methods=['GET']) def confirm_user(data=None): - if not get_config('verify_emails'): + if not utils.get_config('verify_emails'): return redirect(url_for('challenges.challenges_view')) if data and request.method == "GET": # User is confirming email account try: @@ -33,17 +33,17 @@ def confirm_user(data=None): logger = logging.getLogger('regs') logger.warn("[{0}] {1} confirmed {2}".format(time.strftime("%m/%d/%Y %X"), team.name.encode('utf-8'), team.email.encode('utf-8'))) db.session.close() - if authed(): + if utils.authed(): return redirect(url_for('challenges.challenges_view')) return redirect(url_for('auth.login')) if not data and request.method == "GET": # User has been directed to the confirm page because his account is not verified - if not authed(): + if not utils.authed(): return redirect(url_for('auth.login')) team = Teams.query.filter_by(id=session['id']).first_or_404() if team.verified: return redirect(url_for('views.profile')) else: - verify_email(team.email) + utils.verify_email(team.email) return render_template('confirm.html', team=team) @@ -80,7 +80,7 @@ Did you initiate a password reset? """.format(url_for('auth.reset_password', _external=True), urllib.quote_plus(token.encode('base64'))) - sendmail(email, text) + utils.sendmail(email, text) return render_template('reset_password.html', errors=['If that account exists you will receive an email, please check your inbox']) return render_template('reset_password.html') @@ -88,7 +88,7 @@ Did you initiate a password reset? @auth.route('/register', methods=['POST', 'GET']) def register(): - if not can_register(): + if not utils.can_register(): return redirect(url_for('auth.login')) if request.method == 'POST': errors = [] @@ -128,9 +128,9 @@ def register(): session['username'] = team.name session['id'] = team.id session['admin'] = team.admin - session['nonce'] = sha512(os.urandom(10)) + session['nonce'] = utils.sha512(os.urandom(10)) - if can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email. + if utils.can_send_mail() and utils.get_config('verify_emails'): # Confirming users is enabled and we can send email. db.session.close() logger = logging.getLogger('regs') logger.warn("[{0}] {1} registered (UNCONFIRMED) with {2}".format(time.strftime("%m/%d/%Y %X"), @@ -138,8 +138,8 @@ def register(): request.form['email'].encode('utf-8'))) return redirect(url_for('auth.confirm_user')) else: # Don't care about confirming users - if can_send_mail(): # We want to notify the user that they have registered. - sendmail(request.form['email'], "You've successfully registered for {}".format(get_config('ctf_name'))) + if utils.can_send_mail(): # We want to notify the user that they have registered. + utils.sendmail(request.form['email'], "You've successfully registered for {}".format(utils.get_config('ctf_name'))) db.session.close() @@ -165,13 +165,13 @@ def login(): session['username'] = team.name session['id'] = team.id session['admin'] = team.admin - session['nonce'] = sha512(os.urandom(10)) + session['nonce'] = utils.sha512(os.urandom(10)) db.session.close() logger = logging.getLogger('logins') logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'))) - if request.args.get('next') and is_safe_url(request.args.get('next')): + if request.args.get('next') and utils.is_safe_url(request.args.get('next')): return redirect(request.args.get('next')) return redirect(url_for('challenges.challenges_view')) else: # This user exists but the password is wrong @@ -189,6 +189,6 @@ def login(): @auth.route('/logout') def logout(): - if authed(): + if utils.authed(): session.clear() return redirect(url_for('views.static_html')) diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 8394b1a3..cd153f3b 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -6,37 +6,38 @@ import time from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint from sqlalchemy.sql import or_ -from CTFd.utils import ctftime, view_after_ctf, authed, unix_time, get_kpm, user_can_view_challenges, is_admin, get_config, get_ip, is_verified, ctf_started, ctf_ended, ctf_name, hide_scores from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards from CTFd.plugins.keys import get_key_class from CTFd.plugins.challenges import get_chal_class +from CTFd import utils + challenges = Blueprint('challenges', __name__) @challenges.route('/challenges', methods=['GET']) def challenges_view(): errors = [] - start = get_config('start') or 0 - end = get_config('end') or 0 - if not is_admin(): # User is not an admin - if not ctftime(): + start = utils.get_config('start') or 0 + end = utils.get_config('end') or 0 + if not utils.is_admin(): # User is not an admin + if not utils.ctftime(): # It is not CTF time - if view_after_ctf(): # But we are allowed to view after the CTF ends + if utils.view_after_ctf(): # But we are allowed to view after the CTF ends pass else: # We are NOT allowed to view after the CTF ends - if get_config('start') and not ctf_started(): - errors.append('{} has not started yet'.format(ctf_name())) - if (get_config('end') and ctf_ended()) and not view_after_ctf(): - errors.append('{} has ended'.format(ctf_name())) + if utils.get_config('start') and not utils.ctf_started(): + errors.append('{} has not started yet'.format(utils.ctf_name())) + if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf(): + errors.append('{} has ended'.format(utils.ctf_name())) return render_template('chals.html', errors=errors, start=int(start), end=int(end)) - if get_config('verify_emails') and not is_verified(): # User is not confirmed + if utils.get_config('verify_emails') and not utils.is_verified(): # User is not confirmed return redirect(url_for('auth.confirm_user')) - if user_can_view_challenges(): # Do we allow unauthenticated users? - if get_config('start') and not ctf_started(): - errors.append('{} has not started yet'.format(ctf_name())) - if (get_config('end') and ctf_ended()) and not view_after_ctf(): - errors.append('{} has ended'.format(ctf_name())) + if utils.user_can_view_challenges(): # Do we allow unauthenticated users? + if utils.get_config('start') and not utils.ctf_started(): + errors.append('{} has not started yet'.format(utils.ctf_name())) + if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf(): + errors.append('{} has ended'.format(utils.ctf_name())) return render_template('chals.html', errors=errors, start=int(start), end=int(end)) else: return redirect(url_for('auth.login', next='challenges')) @@ -44,13 +45,13 @@ def challenges_view(): @challenges.route('/chals', methods=['GET']) def chals(): - if not is_admin(): - if not ctftime(): - if view_after_ctf(): + if not utils.is_admin(): + if not utils.ctftime(): + if utils.view_after_ctf(): pass else: return redirect(url_for('views.static_html')) - if user_can_view_challenges() and (ctf_started() or is_admin()): + if utils.user_can_view_challenges() and (utils.ctf_started() or utils.is_admin()): chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all() json = {'game': []} for x in chals: @@ -77,14 +78,14 @@ def chals(): @challenges.route('/chals/solves') def solves_per_chal(): - if not user_can_view_challenges(): + if not utils.user_can_view_challenges(): return redirect(url_for('auth.login', next=request.path)) solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves')).join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).group_by(Solves.chalid).subquery() solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves, Challenges.name) \ .join(Challenges, solves_sub.columns.chalid == Challenges.id).all() json = {} - if hide_scores(): + if utils.hide_scores(): for chal, count, name in solves: json[chal] = -1 else: @@ -100,10 +101,10 @@ def solves(teamid=None): solves = None awards = None if teamid is None: - if is_admin(): + if utils.is_admin(): solves = Solves.query.filter_by(teamid=session['id']).all() - elif user_can_view_challenges(): - if authed(): + elif utils.user_can_view_challenges(): + if utils.authed(): solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.teamid == session['id'], Teams.banned == False).all() else: return jsonify({'solves': []}) @@ -121,7 +122,7 @@ def solves(teamid=None): 'team': solve.teamid, 'value': solve.chal.value, 'category': solve.chal.category, - 'time': unix_time(solve.date) + 'time': utils.unix_time(solve.date) }) if awards: for award in awards: @@ -131,7 +132,7 @@ def solves(teamid=None): 'team': award.teamid, 'value': award.value, 'category': award.category or "Award", - 'time': unix_time(award.date) + 'time': utils.unix_time(award.date) }) json['solves'].sort(key=lambda k: k['time']) return jsonify(json) @@ -139,13 +140,13 @@ def solves(teamid=None): @challenges.route('/maxattempts') def attempts(): - if not user_can_view_challenges(): + if not utils.user_can_view_challenges(): return redirect(url_for('auth.login', next=request.path)) chals = Challenges.query.add_columns('id').all() json = {'maxattempts': []} for chal, chalid in chals: fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() - if fails >= int(get_config("max_tries")) and int(get_config("max_tries")) > 0: + if fails >= int(utils.get_config("max_tries")) and int(utils.get_config("max_tries")) > 0: json['maxattempts'].append({'chalid': chalid}) return jsonify(json) @@ -161,11 +162,11 @@ def fails(teamid): @challenges.route('/chal//solves', methods=['GET']) def who_solved(chalid): - if not user_can_view_challenges(): + if not utils.user_can_view_challenges(): return redirect(url_for('auth.login', next=request.path)) json = {'teams': []} - if hide_scores(): + if utils.hide_scores(): return jsonify(json) solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc()) for solve in solves: @@ -175,19 +176,19 @@ def who_solved(chalid): @challenges.route('/chal/', methods=['POST']) def chal(chalid): - if ctf_ended() and not view_after_ctf(): + if utils.ctf_ended() and not utils.view_after_ctf(): return redirect(url_for('challenges.challenges_view')) - if not user_can_view_challenges(): + if not utils.user_can_view_challenges(): return redirect(url_for('auth.login', next=request.path)) - if authed() and is_verified() and (ctf_started() or view_after_ctf()): + if utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf()): fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() logger = logging.getLogger('keys') - data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), get_kpm(session['id'])) + data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), utils.get_kpm(session['id'])) print("[{0}] {1} submitted {2} with kpm {3}".format(*data)) # Anti-bruteforce / submitting keys too quickly - if get_kpm(session['id']) > 10: - if ctftime(): + if utils.get_kpm(session['id']) > 10: + if utils.ctftime(): wrong = WrongKeys(session['id'], chalid, request.form['key']) db.session.add(wrong) db.session.commit() @@ -214,15 +215,15 @@ def chal(chalid): chal_class = get_chal_class(chal.type) if chal_class.solve(chal, provided_key): - if ctftime(): - solve = Solves(chalid=chalid, teamid=session['id'], ip=get_ip(), flag=provided_key) + if utils.ctftime(): + solve = Solves(chalid=chalid, teamid=session['id'], ip=utils.get_ip(), flag=provided_key) db.session.add(solve) db.session.commit() db.session.close() logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) return jsonify({'status': '1', 'message': 'Correct'}) - if ctftime(): + if utils.ctftime(): wrong = WrongKeys(teamid=session['id'], chalid=chalid, flag=provided_key) db.session.add(wrong) db.session.commit() diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index e351949a..67141b9c 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -1,9 +1,10 @@ from flask import render_template, jsonify, Blueprint, redirect, url_for, request from sqlalchemy.sql.expression import union_all -from CTFd.utils import unix_time, authed, get_config, hide_scores from CTFd.models import db, Teams, Solves, Awards, Challenges +from CTFd import utils + scoreboard = Blueprint('scoreboard', __name__) @@ -35,9 +36,9 @@ def get_standings(admin=False, count=None): @scoreboard.route('/scoreboard') def scoreboard_view(): - if get_config('view_scoreboard_if_authed') and not authed(): + if utils.get_config('view_scoreboard_if_authed') and not utils.authed(): return redirect(url_for('auth.login', next=request.path)) - if hide_scores(): + if utils.hide_scores(): return render_template('scoreboard.html', errors=['Scores are currently hidden']) standings = get_standings() return render_template('scoreboard.html', teams=standings) @@ -46,9 +47,9 @@ def scoreboard_view(): @scoreboard.route('/scores') def scores(): json = {'standings': []} - if get_config('view_scoreboard_if_authed') and not authed(): + if utils.get_config('view_scoreboard_if_authed') and not utils.authed(): return redirect(url_for('auth.login', next=request.path)) - if hide_scores(): + if utils.hide_scores(): return jsonify(json) standings = get_standings() @@ -61,9 +62,9 @@ def scores(): @scoreboard.route('/top/') def topteams(count): json = {'scores': {}} - if get_config('view_scoreboard_if_authed') and not authed(): + if utils.get_config('view_scoreboard_if_authed') and not utils.authed(): return redirect(url_for('auth.login', next=request.path)) - if hide_scores(): + if utils.hide_scores(): return jsonify(json) if count > 20 or count < 0: @@ -80,14 +81,14 @@ def topteams(count): 'chal': x.chalid, 'team': x.teamid, 'value': x.chal.value, - 'time': unix_time(x.date) + 'time': utils.unix_time(x.date) }) for award in awards: json['scores'][team.name].append({ 'chal': None, 'team': award.teamid, 'value': award.value, - 'time': unix_time(award.date) + 'time': utils.unix_time(award.date) }) json['scores'][team.name] = sorted(json['scores'][team.name], key=lambda k: k['time']) return jsonify(json) diff --git a/CTFd/templates/admin/teams.html b/CTFd/templates/admin/teams.html index 72bc8d95..623f9416 100644 --- a/CTFd/templates/admin/teams.html +++ b/CTFd/templates/admin/teams.html @@ -198,8 +198,10 @@ $('#update-user').click(function(e){ console.log($.grep(user_data, function(e){ return e.name == 'name'; })[0]['value']) console.log(row.find('.team-name > a')) row.find('.team-name > a').text( $.grep(user_data, function(e){ return e.name == 'name'; })[0]['value'] ); - row.find('.team-email').text( $.grep(user_data, function(e){ return e.name == 'email'; })[0]['value'] ); - + var new_email = $.grep(user_data, function(e){ return e.name == 'email'; })[0]['value']; + if (new_email){ + row.find('.team-email').text( new_email ); + } row.find('.team-website > a').empty() var website = $.grep(user_data, function(e){ return e.name == 'website'; })[0]['value'] row.find('.team-website').append($('').attr('href', website).text(website)); diff --git a/CTFd/views.py b/CTFd/views.py index 65a60463..55882a9b 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -5,9 +5,9 @@ from flask import current_app as app, render_template, request, redirect, abort, from jinja2.exceptions import TemplateNotFound from passlib.hash import bcrypt_sha256 -from CTFd.utils import authed, is_setup, validate_url, get_config, set_config, sha512, cache, ctftime, view_after_ctf, ctf_started, \ - is_admin, hide_scores from CTFd.models import db, Teams, Solves, Awards, Files, Pages +from CTFd.utils import cache +from CTFd import utils views = Blueprint('views', __name__) @@ -16,7 +16,7 @@ views = Blueprint('views', __name__) def redirect_setup(): if request.path.startswith("/static"): return - if not is_setup() and request.path != "/setup": + if not utils.is_setup() and request.path != "/setup": return redirect(url_for('views.setup')) @@ -25,15 +25,15 @@ def setup(): # with app.app_context(): # admin = Teams.query.filter_by(admin=True).first() - if not is_setup(): + if not utils.is_setup(): if not session.get('nonce'): - session['nonce'] = sha512(os.urandom(10)) + session['nonce'] = utils.sha512(os.urandom(10)) if request.method == 'POST': ctf_name = request.form['ctf_name'] - ctf_name = set_config('ctf_name', ctf_name) + ctf_name = utils.set_config('ctf_name', ctf_name) # CSS - css = set_config('start', '') + css = utils.set_config('start', '') # Admin user name = request.form['name'] @@ -56,29 +56,29 @@ def setup(): """.format(request.script_root)) # max attempts per challenge - max_tries = set_config("max_tries", 0) + max_tries = utils.set_config("max_tries", 0) # Start time - start = set_config('start', None) - end = set_config('end', None) + start = utils.set_config('start', None) + end = utils.set_config('end', None) # Challenges cannot be viewed by unregistered users - view_challenges_unregistered = set_config('view_challenges_unregistered', None) + view_challenges_unregistered = utils.set_config('view_challenges_unregistered', None) # Allow/Disallow registration - prevent_registration = set_config('prevent_registration', None) + prevent_registration = utils.set_config('prevent_registration', None) # Verify emails - verify_emails = set_config('verify_emails', None) + verify_emails = utils.set_config('verify_emails', None) - mail_server = set_config('mail_server', None) - mail_port = set_config('mail_port', None) - mail_tls = set_config('mail_tls', None) - mail_ssl = set_config('mail_ssl', None) - mail_username = set_config('mail_username', None) - mail_password = set_config('mail_password', None) + mail_server = utils.set_config('mail_server', None) + mail_port = utils.set_config('mail_port', None) + mail_tls = utils.set_config('mail_tls', None) + mail_ssl = utils.set_config('mail_ssl', None) + mail_username = utils.set_config('mail_username', None) + mail_password = utils.set_config('mail_password', None) - setup = set_config('setup', True) + setup = utils.set_config('setup', True) db.session.add(page) db.session.add(admin) @@ -87,7 +87,7 @@ def setup(): session['username'] = admin.name session['id'] = admin.id session['admin'] = admin.admin - session['nonce'] = sha512(os.urandom(10)) + session['nonce'] = utils.sha512(os.urandom(10)) db.session.close() app.setup = False @@ -102,7 +102,7 @@ def setup(): # Custom CSS handler @views.route('/static/user.css') def custom_css(): - return Response(get_config("css"), mimetype='text/css') + return Response(utils.get_config("css"), mimetype='text/css') # Static HTML files @@ -124,7 +124,7 @@ def teams(page): page_start = results_per_page * (page - 1) page_end = results_per_page * (page - 1) + results_per_page - if get_config('verify_emails'): + if utils.get_config('verify_emails'): count = Teams.query.filter_by(verified=True, banned=False).count() teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all() else: @@ -136,7 +136,7 @@ def teams(page): @views.route('/team/', methods=['GET', 'POST']) def team(teamid): - if get_config('view_scoreboard_if_authed') and not authed(): + if utils.get_config('view_scoreboard_if_utils.authed') and not utils.authed(): return redirect(url_for('auth.login', next=request.path)) errors = [] user = Teams.query.filter_by(id=teamid).first_or_404() @@ -146,7 +146,7 @@ def team(teamid): place = user.place() db.session.close() - if hide_scores() and teamid != session.get('id'): + if utils.hide_scores() and teamid != session.get('id'): errors.append('Scores are currently hidden') if errors: @@ -163,7 +163,7 @@ def team(teamid): @views.route('/profile', methods=['POST', 'GET']) def profile(): - if authed(): + if utils.authed(): if request.method == "POST": errors = [] @@ -175,7 +175,7 @@ def profile(): user = Teams.query.filter_by(id=session['id']).first() - if not get_config('prevent_name_change'): + if not utils.get_config('prevent_name_change'): names = Teams.query.filter_by(name=name).first() name_len = len(request.form['name']) == 0 @@ -187,13 +187,13 @@ def profile(): errors.append("Your old password doesn't match what we have.") if not valid_email: errors.append("That email doesn't look right") - if not get_config('prevent_name_change') and names and name != session['username']: + if not utils.get_config('prevent_name_change') and names and name != session['username']: errors.append('That team name is already taken') if emails and emails.id != session['id']: errors.append('That email has already been used') - if not get_config('prevent_name_change') and name_len: + if not utils.get_config('prevent_name_change') and name_len: errors.append('Pick a longer team name') - if website.strip() and not validate_url(website): + if website.strip() and not utils.validate_url(website): errors.append("That doesn't look like a valid URL") if len(errors) > 0: @@ -201,11 +201,11 @@ def profile(): affiliation=affiliation, country=country, errors=errors) else: team = Teams.query.filter_by(id=session['id']).first() - if not get_config('prevent_name_change'): + if not utils.get_config('prevent_name_change'): team.name = name if team.email != email.lower(): team.email = email.lower() - if get_config('verify_emails'): + if utils.get_config('verify_emails'): team.verified = False session['username'] = team.name @@ -224,8 +224,8 @@ def profile(): website = user.website affiliation = user.affiliation country = user.country - prevent_name_change = get_config('prevent_name_change') - confirm_email = get_config('verify_emails') and not user.verified + prevent_name_change = utils.get_config('prevent_name_change') + confirm_email = utils.get_config('verify_emails') and not user.verified return render_template('profile.html', name=name, email=email, website=website, affiliation=affiliation, country=country, prevent_name_change=prevent_name_change, confirm_email=confirm_email) else: @@ -237,9 +237,9 @@ def profile(): def file_handler(path): f = Files.query.filter_by(location=path).first_or_404() if f.chal: - if not is_admin(): - if not ctftime(): - if view_after_ctf() and ctf_started(): + if not utils.is_admin(): + if not utils.ctftime(): + if utils.view_after_ctf() and utils.ctf_started(): pass else: abort(403) diff --git a/tests/helpers.py b/tests/helpers.py index e6a53b53..f350f8ab 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,4 +1,5 @@ from CTFd import create_app +from CTFd.models import * from sqlalchemy_utils import database_exists, create_database, drop_database from sqlalchemy.engine.url import make_url @@ -57,4 +58,63 @@ def login_as_user(app, name="user", password="password"): "nonce": sess.get('nonce') } client.post('/login', data=data) - return client \ No newline at end of file + return client + + +def gen_challenge(db, name='chal_name', description='chal_description', value=100, category='chal_category', type=0): + chal = Challenges(name, description, value, category) + db.session.add(chal) + db.session.commit() + return chal + + +def gen_award(db, teamid, name="award_name", value=100): + award = Awards(teamid, name, value) + db.session.add(award) + db.session.commit() + return award + + +def gen_tag(db, chal, tag='tag_tag'): + tag = Tags(chal, tag) + db.session.add(tag) + db.session.commit() + return tag + + +def gen_file(): + pass + + +def gen_key(db, chal, flag='flag', key_type=0): + key = Keys(chal, flag, key_type) + db.session.add(key) + db.session.commit() + return key + + +def gen_team(db, name='name', email='user@ctfd.io', password='password'): + team = Teams(name, email, password) + db.session.add(team) + db.session.commit() + return team + + +def gen_solve(db, chalid, teamid, ip='127.0.0.1', flag='rightkey'): + solve = Solves(chalid, teamid, ip, flag) + db.session.add(solve) + db.session.commit() + return solve + +def gen_wrongkey(db, teamid, chalid, flag='wrongkey'): + wrongkey = WrongKeys(teamid, chalid, flag) + db.session.add(wrongkey) + db.session.commit() + return wrongkey + + +def gen_tracking(db, ip, team): + tracking = Tracking(ip, team) + db.session.add(tracking) + db.session.commit() + return tracking \ No newline at end of file diff --git a/tests/test_admin_facing.py b/tests/test_admin_facing.py index e69de29b..eba1d1af 100644 --- a/tests/test_admin_facing.py +++ b/tests/test_admin_facing.py @@ -0,0 +1,76 @@ +from tests.helpers import create_ctfd, register_user, login_as_user +from CTFd.models import Teams + + +def test_admin_panel(): + """Does the admin panel return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin') + assert r.status_code == 302 + r = client.get('/admin/graphs') + assert r.status_code == 200 + + +def test_admin_pages(): + """Does admin pages return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/pages') + assert r.status_code == 200 + + +def test_admin_teams(): + """Does admin teams return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/teams') + assert r.status_code == 200 + + +def test_admin_scoreboard(): + """Does admin scoreboard return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/scoreboard') + assert r.status_code == 200 + + +def test_admin_containers(): + """Does admin containers return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/containers') + assert r.status_code == 200 + + +def test_admin_chals(): + """Does admin chals return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/chals') + assert r.status_code == 200 + + +def test_admin_statistics(): + """Does admin statistics return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/statistics') + assert r.status_code == 200 + + +def test_admin_config(): + """Does admin config return a 200 by default""" + app = create_ctfd() + with app.app_context(): + client = login_as_user(app, name="admin", password="password") + r = client.get('/admin/config') + assert r.status_code == 200 \ No newline at end of file diff --git a/tests/test_user_facing.py b/tests/test_user_facing.py index 3761b222..8ac9c4a1 100644 --- a/tests/test_user_facing.py +++ b/tests/test_user_facing.py @@ -1,5 +1,6 @@ -from tests.helpers import create_ctfd, register_user, login_as_user +from tests.helpers import create_ctfd, register_user, login_as_user, gen_challenge from CTFd.models import Teams +import json def test_index(): @@ -191,4 +192,16 @@ def test_user_get_reset_password(): register_user(app) client = app.test_client() r = client.get('/reset_password') - assert r.status_code == 200 \ No newline at end of file + assert r.status_code == 200 + + +def test_viewing_challenges(): + """Test that users can see added challenges""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + gen_challenge(app.db) + r = client.get('/chals') + chals = json.loads(r.data) + assert len(chals['game']) == 1 \ No newline at end of file