mirror of https://github.com/JohnHammond/CTFd.git
Merge pull request #1359 from CTFd/table-granular-admin-reset
* Improve Reset functionality by allowing admins to pick what data they'd like to reset * Closes #1355remove-get-config-from-models
commit
e15e23f038
|
@ -179,6 +179,18 @@ def create_app(config="CTFd.config.Config"):
|
|||
|
||||
# Alembic sqlite support is lacking so we should just create_all anyway
|
||||
if url.drivername.startswith("sqlite"):
|
||||
# Enable foreign keys for SQLite. This must be before the
|
||||
# db.create_all call because tests use the in-memory SQLite
|
||||
# database (each connection, including db creation, is a new db).
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy import event
|
||||
|
||||
@event.listens_for(Engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close()
|
||||
|
||||
db.create_all()
|
||||
stamp_latest_revision()
|
||||
else:
|
||||
|
|
|
@ -14,10 +14,13 @@ from flask import (
|
|||
url_for,
|
||||
)
|
||||
|
||||
from CTFd.cache import cache, clear_config
|
||||
from CTFd.cache import cache, clear_config, clear_standings, clear_pages
|
||||
from CTFd.models import (
|
||||
Awards,
|
||||
Challenges,
|
||||
Configs,
|
||||
Notifications,
|
||||
Pages,
|
||||
Solves,
|
||||
Submissions,
|
||||
Teams,
|
||||
|
@ -34,6 +37,7 @@ 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 = Blueprint("admin", __name__)
|
||||
|
@ -176,19 +180,60 @@ def config():
|
|||
@admins_only
|
||||
def reset():
|
||||
if request.method == "POST":
|
||||
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
|
||||
Tracking.query.delete()
|
||||
Solves.query.delete()
|
||||
Submissions.query.delete()
|
||||
Awards.query.delete()
|
||||
Unlocks.query.delete()
|
||||
Users.query.delete()
|
||||
Teams.query.delete()
|
||||
set_config("setup", False)
|
||||
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()
|
||||
cache.clear()
|
||||
logout_user()
|
||||
|
||||
clear_pages()
|
||||
clear_standings()
|
||||
clear_config()
|
||||
|
||||
if logout is True:
|
||||
cache.clear()
|
||||
logout_user()
|
||||
|
||||
db.session.close()
|
||||
return redirect(url_for("views.setup"))
|
||||
return redirect(next_url)
|
||||
|
||||
return render_template("admin/reset.html")
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
or choose to form teams.
|
||||
</small>
|
||||
</label>
|
||||
<div data-toggle="tooltip" data-placement="bottom" title="In order to change User Mode you must reset your CTF">
|
||||
<div data-toggle="tooltip" data-placement="bottom" title="In order to change User Mode you must reset your CTF and delete all accounts">
|
||||
<select class="form-control custom-select" id="user_mode" name="user_mode" disabled="true" style="z-index: -1;">
|
||||
<option value="teams" {% if user_mode == 'teams' %}selected{% endif %}>
|
||||
Teams
|
||||
|
|
|
@ -15,18 +15,89 @@
|
|||
<form method="POST" id="reset-ctf-form">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p>
|
||||
Resetting your CTF will delete all user and team data. Think carefully before resetting because
|
||||
no automated backups are made and all non-challenge data is lost.
|
||||
Resetting your CTFd instance allows you to bulk delete data to prepare it for other events,
|
||||
other classes, or to otherwise get it to a clean state.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Resetting your CTFd instance will delete the selected data <strong>PERMANENTLY</strong>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Think carefully before resetting because no automated backups are made and all selected data will be lost.
|
||||
</p>
|
||||
|
||||
<span>
|
||||
<strong>
|
||||
Create backups of all data you need by <a href="{{ url_for('admin.config', _anchor='backup') }}">creating a CTFd Export</a>
|
||||
or by copying the database and CTFd source code folder.
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group pb-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" name="accounts"> Accounts
|
||||
</label>
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
Deletes all user and team accounts and their associated information<br>
|
||||
<small>(Users, Teams, Submissions, Tracking)</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group pb-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" name="submissions"> Submissions
|
||||
</label>
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
Deletes all records that accounts gained points or took an action<br>
|
||||
<small>(Submissions, Awards, Unlocks, Tracking)</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group pb-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" name="challenges"> Challenges
|
||||
</label>
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
Deletes all challenges and associated data<br>
|
||||
<small>(Challenges, Flags, Hints, Tags, Challenge Files)</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group pb-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" name="pages"> Pages
|
||||
</label>
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
Deletes all pages and their associated files<br>
|
||||
<small>(Pages, Page Files)</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group pb-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" name="notifications"> Notifications
|
||||
</label>
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
Deletes all notifications<br>
|
||||
<small>(Notifications)</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
|
||||
<button class="btn btn-warning btn-lg btn-block">
|
||||
|
|
|
@ -7,6 +7,7 @@ import random
|
|||
import argparse
|
||||
|
||||
from CTFd import create_app
|
||||
from CTFd.cache import clear_config, clear_standings, clear_pages
|
||||
from CTFd.models import (
|
||||
Users,
|
||||
Teams,
|
||||
|
@ -338,3 +339,7 @@ if __name__ == "__main__":
|
|||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
clear_config()
|
||||
clear_standings()
|
||||
clear_pages()
|
||||
|
|
|
@ -1,19 +1,36 @@
|
|||
import random
|
||||
|
||||
from CTFd.models import Challenges, Fails, Solves, Teams, Tracking, Users
|
||||
from CTFd.models import (
|
||||
Awards,
|
||||
Challenges,
|
||||
Fails,
|
||||
Files,
|
||||
Flags,
|
||||
Hints,
|
||||
Notifications,
|
||||
Pages,
|
||||
Solves,
|
||||
Submissions,
|
||||
Tags,
|
||||
Teams,
|
||||
Tracking,
|
||||
Unlocks,
|
||||
Users,
|
||||
)
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_file,
|
||||
gen_flag,
|
||||
gen_hint,
|
||||
gen_solve,
|
||||
gen_team,
|
||||
gen_tracking,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
|
@ -25,6 +42,12 @@ def test_reset():
|
|||
for x in range(10):
|
||||
chal = gen_challenge(app.db, name="chal_name{}".format(x))
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
gen_hint(app.db, challenge_id=chal.id)
|
||||
gen_file(
|
||||
app.db,
|
||||
location="{name}/{name}.file".format(name=chal.name),
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
|
||||
for x in range(10):
|
||||
user = base_user + str(x)
|
||||
|
@ -35,18 +58,77 @@ def test_reset():
|
|||
gen_fail(app.db, user_id=user_obj.id, challenge_id=random.randint(1, 10))
|
||||
gen_tracking(app.db, user_id=user_obj.id)
|
||||
|
||||
# Add PageFiles
|
||||
for x in range(5):
|
||||
gen_file(
|
||||
app.db,
|
||||
location="page_file{name}/page_file{name}.file".format(name=x),
|
||||
page_id=1,
|
||||
)
|
||||
|
||||
assert Users.query.count() == 11 # 11 because of the first admin user
|
||||
assert Challenges.query.count() == 10
|
||||
assert (
|
||||
Files.query.count() == 15
|
||||
) # This should be 11 because ChallengeFiles=10 and PageFiles=5
|
||||
assert Flags.query.count() == 10
|
||||
assert Hints.query.count() == 10
|
||||
assert Submissions.query.count() == 20
|
||||
assert Pages.query.count() == 1
|
||||
assert Tracking.query.count() == 10
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce")}
|
||||
client.post("/admin/reset", data=data)
|
||||
|
||||
assert Users.query.count() == 0
|
||||
data = {"nonce": sess.get("nonce"), "pages": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Pages.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
assert Files.query.count() == 10
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "notifications": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Notifications.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "challenges": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Challenges.query.count() == 0
|
||||
assert Flags.query.count() == 0
|
||||
assert Hints.query.count() == 0
|
||||
assert Files.query.count() == 0
|
||||
assert Tags.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "submissions": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Submissions.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Awards.query.count() == 0
|
||||
assert Unlocks.query.count() == 0
|
||||
assert Users.query.count() == 11
|
||||
assert Challenges.query.count() == 0
|
||||
assert Flags.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "accounts": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/setup")
|
||||
assert Users.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
|
@ -62,6 +144,12 @@ def test_reset_team_mode():
|
|||
for x in range(10):
|
||||
chal = gen_challenge(app.db, name="chal_name{}".format(x))
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
gen_hint(app.db, challenge_id=chal.id)
|
||||
gen_file(
|
||||
app.db,
|
||||
location="{name}/{name}.file".format(name=chal.name),
|
||||
challenge_id=chal.id,
|
||||
)
|
||||
|
||||
for x in range(10):
|
||||
user = base_user + str(x)
|
||||
|
@ -78,22 +166,85 @@ def test_reset_team_mode():
|
|||
gen_fail(app.db, user_id=user_obj.id, challenge_id=random.randint(1, 10))
|
||||
gen_tracking(app.db, user_id=user_obj.id)
|
||||
|
||||
assert Teams.query.count() == 10
|
||||
assert (
|
||||
Users.query.count() == 51
|
||||
) # 10 random users, 40 users (10 teams * 4), 1 admin user
|
||||
assert Challenges.query.count() == 10
|
||||
# Add PageFiles
|
||||
for x in range(5):
|
||||
gen_file(
|
||||
app.db,
|
||||
location="page_file{name}/page_file{name}.file".format(name=x),
|
||||
page_id=1,
|
||||
)
|
||||
|
||||
assert Teams.query.count() == 10
|
||||
# 10 random users, 40 users (10 teams * 4), 1 admin user
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 10
|
||||
assert (
|
||||
Files.query.count() == 15
|
||||
) # This should be 11 because ChallengeFiles=10 and PageFiles=5
|
||||
assert Flags.query.count() == 10
|
||||
assert Hints.query.count() == 10
|
||||
assert Submissions.query.count() == 20
|
||||
assert Solves.query.count() == 10
|
||||
assert Fails.query.count() == 10
|
||||
assert Tracking.query.count() == 10
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce")}
|
||||
client.post("/admin/reset", data=data)
|
||||
|
||||
assert Teams.query.count() == 0
|
||||
assert Users.query.count() == 0
|
||||
data = {"nonce": sess.get("nonce"), "pages": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Pages.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
assert Files.query.count() == 10
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "notifications": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Notifications.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 10
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "challenges": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Challenges.query.count() == 0
|
||||
assert Flags.query.count() == 0
|
||||
assert Hints.query.count() == 0
|
||||
assert Files.query.count() == 0
|
||||
assert Tags.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Tracking.query.count() == 11
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "submissions": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/admin/statistics")
|
||||
assert Submissions.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Awards.query.count() == 0
|
||||
assert Unlocks.query.count() == 0
|
||||
assert Teams.query.count() == 10
|
||||
assert Users.query.count() == 51
|
||||
assert Challenges.query.count() == 0
|
||||
assert Flags.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "accounts": "on"}
|
||||
r = client.post("/admin/reset", data=data)
|
||||
assert r.location.endswith("/setup")
|
||||
assert Users.query.count() == 0
|
||||
assert Teams.query.count() == 0
|
||||
assert Solves.query.count() == 0
|
||||
assert Fails.query.count() == 0
|
||||
assert Tracking.query.count() == 0
|
||||
|
|
Loading…
Reference in New Issue