Files
CTFd/CTFd/admin/__init__.py
Kevin Chung 8de9819bd4 3.3.0 (#1833)
# 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`
2021-03-18 18:08:46 -04:00

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")