mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-18 22:44:24 +01:00
# 3.3.0 / UNRELEASED
**General**
- Don't require a team for viewing challenges if Challenge visibility is set to public
- Add a `THEME_FALLBACK` config to help develop themes. See **Themes** section for details.
**API**
- Implement a faster `/api/v1/scoreboard` endpoint in Teams Mode
- Add the `solves` item to both `/api/v1/challenges` and `/api/v1/challenges/[challenge_id]` to more easily determine how many solves a challenge has
- Add the `solved_by_me` item to both `/api/v1/challenges` and `/api/v1/challenges/[challenge_id]` to more easily determine if the current account has solved the challenge
- Prevent admins from deleting themselves through `DELETE /api/v1/users/[user_id]`
- Add length checking to some sensitive fields in the Pages and Challenges schemas
- Fix issue where `PATCH /api/v1/users[user_id]` returned a list instead of a dict
- Fix exception that occured on demoting admins through `PATCH /api/v1/users[user_id]`
- Add `team_id` to `GET /api/v1/users` to determine if a user is already in a team
**Themes**
- Add a `THEME_FALLBACK` config to help develop themes.
- `THEME_FALLBACK` will configure CTFd to try to find missing theme files in the default built-in `core` theme.
- This makes it easier to develop themes or use incomplete themes.
- Allow for one theme to reference and inherit from another theme through approaches like `{% extends "core/page.html" %}`
- Allow for the automatic date rendering format to be overridden by specifying a `data-time-format` attribute.
- Add styling for the `<blockquote>` element.
- Fix scoreboard table identifier to switch between User/Team depending on configured user mode
- Switch to using Bootstrap's scss in `core/main.scss` to allow using Bootstrap variables
- Consolidate Jinja error handlers into a single function and better handle issues where error templates can't be found
**Plugins**
- Set plugin migration version after successful migrations
- Fix issue where Page URLs injected into the navbar were relative instead of absolute
**Admin Panel**
- Add User standings as well as Teams standings to the admin scoreboard when in Teams Mode
- Add a UI for adding members to a team from the team's admin page
- Add ability for admins to disable public team creation
- Link directly to users who submitted something in the submissions page if the CTF is in Teams Mode
- Fix Challenge Requirements interface in Admin Panel to not allow empty/null requirements to be added
- Fixed an issue where config times (start, end, freeze times) could not be removed
- Fix an exception that occurred when demoting an Admin user
- Adds a temporary hack for re-enabling Javascript snippets in Flag editor templates. (See #1779)
**Deployment**
- Install `python3-dev` instead of `python-dev` in apt
- Bump lxml to 4.6.2
- Bump pip-compile to 5.4.0
**Miscellaneous**
- Cache Docker builds more by copying and installing Python dependencies before copying CTFd
- Change the default emails slightly and rework confirmation email page to make some recommendations clearer
- Use `examplectf.com` as testing/development domain instead of `ctfd.io`
- Fixes issue where user's name and email would not appear in logs properly
- Add more linting by also linting with `flake8-comprehensions` and `flake8-bugbear`
236 lines
6.1 KiB
Python
236 lines
6.1 KiB
Python
import csv
|
|
import datetime
|
|
import os
|
|
from io import BytesIO, StringIO
|
|
|
|
from flask import Blueprint, abort
|
|
from flask import current_app as app
|
|
from flask import (
|
|
redirect,
|
|
render_template,
|
|
render_template_string,
|
|
request,
|
|
send_file,
|
|
url_for,
|
|
)
|
|
|
|
admin = Blueprint("admin", __name__)
|
|
|
|
# isort:imports-firstparty
|
|
from CTFd.admin import challenges # noqa: F401
|
|
from CTFd.admin import notifications # noqa: F401
|
|
from CTFd.admin import pages # noqa: F401
|
|
from CTFd.admin import scoreboard # noqa: F401
|
|
from CTFd.admin import statistics # noqa: F401
|
|
from CTFd.admin import submissions # noqa: F401
|
|
from CTFd.admin import teams # noqa: F401
|
|
from CTFd.admin import users # noqa: F401
|
|
from CTFd.cache import cache, clear_config, clear_pages, clear_standings
|
|
from CTFd.models import (
|
|
Awards,
|
|
Challenges,
|
|
Configs,
|
|
Notifications,
|
|
Pages,
|
|
Solves,
|
|
Submissions,
|
|
Teams,
|
|
Tracking,
|
|
Unlocks,
|
|
Users,
|
|
db,
|
|
get_class_by_tablename,
|
|
)
|
|
from CTFd.utils import config as ctf_config
|
|
from CTFd.utils import get_config, set_config
|
|
from CTFd.utils.decorators import admins_only
|
|
from CTFd.utils.exports import export_ctf as export_ctf_util
|
|
from CTFd.utils.exports import import_ctf as import_ctf_util
|
|
from CTFd.utils.helpers import get_errors
|
|
from CTFd.utils.security.auth import logout_user
|
|
from CTFd.utils.uploads import delete_file
|
|
from CTFd.utils.user import is_admin
|
|
|
|
|
|
@admin.route("/admin", methods=["GET"])
|
|
def view():
|
|
if is_admin():
|
|
return redirect(url_for("admin.statistics"))
|
|
return redirect(url_for("auth.login"))
|
|
|
|
|
|
@admin.route("/admin/plugins/<plugin>", methods=["GET", "POST"])
|
|
@admins_only
|
|
def plugin(plugin):
|
|
if request.method == "GET":
|
|
plugins_path = os.path.join(app.root_path, "plugins")
|
|
|
|
config_html_plugins = [
|
|
name
|
|
for name in os.listdir(plugins_path)
|
|
if os.path.isfile(os.path.join(plugins_path, name, "config.html"))
|
|
]
|
|
|
|
if plugin in config_html_plugins:
|
|
config_html = open(
|
|
os.path.join(app.root_path, "plugins", plugin, "config.html")
|
|
).read()
|
|
return render_template_string(config_html)
|
|
abort(404)
|
|
elif request.method == "POST":
|
|
for k, v in request.form.items():
|
|
if k == "nonce":
|
|
continue
|
|
set_config(k, v)
|
|
with app.app_context():
|
|
clear_config()
|
|
return "1"
|
|
|
|
|
|
@admin.route("/admin/import", methods=["POST"])
|
|
@admins_only
|
|
def import_ctf():
|
|
backup = request.files["backup"]
|
|
errors = get_errors()
|
|
try:
|
|
import_ctf_util(backup)
|
|
except Exception as e:
|
|
print(e)
|
|
errors.append(repr(e))
|
|
|
|
if errors:
|
|
return errors[0], 500
|
|
else:
|
|
return redirect(url_for("admin.config"))
|
|
|
|
|
|
@admin.route("/admin/export", methods=["GET", "POST"])
|
|
@admins_only
|
|
def export_ctf():
|
|
backup = export_ctf_util()
|
|
ctf_name = ctf_config.ctf_name()
|
|
day = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
full_name = u"{}.{}.zip".format(ctf_name, day)
|
|
return send_file(
|
|
backup, cache_timeout=-1, as_attachment=True, attachment_filename=full_name
|
|
)
|
|
|
|
|
|
@admin.route("/admin/export/csv")
|
|
@admins_only
|
|
def export_csv():
|
|
table = request.args.get("table")
|
|
|
|
# TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information.
|
|
model = get_class_by_tablename(table)
|
|
if model is None:
|
|
abort(404)
|
|
|
|
temp = StringIO()
|
|
writer = csv.writer(temp)
|
|
|
|
header = [column.name for column in model.__mapper__.columns]
|
|
writer.writerow(header)
|
|
|
|
responses = model.query.all()
|
|
|
|
for curr in responses:
|
|
writer.writerow(
|
|
[getattr(curr, column.name) for column in model.__mapper__.columns]
|
|
)
|
|
|
|
temp.seek(0)
|
|
|
|
# In Python 3 send_file requires bytes
|
|
output = BytesIO()
|
|
output.write(temp.getvalue().encode("utf-8"))
|
|
output.seek(0)
|
|
temp.close()
|
|
|
|
return send_file(
|
|
output,
|
|
as_attachment=True,
|
|
cache_timeout=-1,
|
|
attachment_filename="{name}-{table}.csv".format(
|
|
name=ctf_config.ctf_name(), table=table
|
|
),
|
|
)
|
|
|
|
|
|
@admin.route("/admin/config", methods=["GET", "POST"])
|
|
@admins_only
|
|
def config():
|
|
# Clear the config cache so that we don't get stale values
|
|
clear_config()
|
|
|
|
configs = Configs.query.all()
|
|
configs = {c.key: get_config(c.key) for c in configs}
|
|
|
|
themes = ctf_config.get_themes()
|
|
themes.remove(get_config("ctf_theme"))
|
|
|
|
return render_template("admin/config.html", themes=themes, **configs)
|
|
|
|
|
|
@admin.route("/admin/reset", methods=["GET", "POST"])
|
|
@admins_only
|
|
def reset():
|
|
if request.method == "POST":
|
|
require_setup = False
|
|
logout = False
|
|
next_url = url_for("admin.statistics")
|
|
|
|
data = request.form
|
|
|
|
if data.get("pages"):
|
|
_pages = Pages.query.all()
|
|
for p in _pages:
|
|
for f in p.files:
|
|
delete_file(file_id=f.id)
|
|
|
|
Pages.query.delete()
|
|
|
|
if data.get("notifications"):
|
|
Notifications.query.delete()
|
|
|
|
if data.get("challenges"):
|
|
_challenges = Challenges.query.all()
|
|
for c in _challenges:
|
|
for f in c.files:
|
|
delete_file(file_id=f.id)
|
|
Challenges.query.delete()
|
|
|
|
if data.get("accounts"):
|
|
Users.query.delete()
|
|
Teams.query.delete()
|
|
require_setup = True
|
|
logout = True
|
|
|
|
if data.get("submissions"):
|
|
Solves.query.delete()
|
|
Submissions.query.delete()
|
|
Awards.query.delete()
|
|
Unlocks.query.delete()
|
|
Tracking.query.delete()
|
|
|
|
if require_setup:
|
|
set_config("setup", False)
|
|
cache.clear()
|
|
logout_user()
|
|
next_url = url_for("views.setup")
|
|
|
|
db.session.commit()
|
|
|
|
clear_pages()
|
|
clear_standings()
|
|
clear_config()
|
|
|
|
if logout is True:
|
|
cache.clear()
|
|
logout_user()
|
|
|
|
db.session.close()
|
|
return redirect(next_url)
|
|
|
|
return render_template("admin/reset.html")
|