Improve imports/exports to reduce the likelihood of a conflict (#523)

* Improve imports/exports to reduce the likelihood of a conflict
* To allow previous version imports of keys to work - fill in key `type` from `key_type`. (#520)
* Adding an import_ctf test
This commit is contained in:
Kevin Chung
2017-12-12 05:43:27 -05:00
committed by GitHub
parent f23fd627ed
commit a571cf1baf
2 changed files with 85 additions and 9 deletions

View File

@@ -32,7 +32,7 @@ from sqlalchemy.exc import InvalidRequestError, IntegrityError
from socket import timeout from socket import timeout
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, ip2long, long2ip from CTFd.models import db, Challenges, WrongKeys, Pages, Config, Tracking, Teams, Files, ip2long, long2ip
from datafreeze.format import SERIALIZERS from datafreeze.format import SERIALIZERS
from datafreeze.format.fjson import JSONSerializer, JSONEncoder from datafreeze.format.fjson import JSONSerializer, JSONEncoder
@@ -793,6 +793,14 @@ def export_ctf(segments=None):
result_file.seek(0) result_file.seek(0)
backup_zip.writestr('db/{}.json'.format(item), result_file.read()) backup_zip.writestr('db/{}.json'.format(item), result_file.read())
# Guarantee that alembic_version is saved into the export
if 'metadata' not in segments:
result = db['alembic_version'].all()
result_file = six.BytesIO()
datafreeze.freeze(result, format='ctfd', fileobj=result_file)
result_file.seek(0)
backup_zip.writestr('db/alembic_version.json', result_file.read())
# Backup uploads # Backup uploads
upload_folder = os.path.join(os.path.normpath(app.root_path), get_config('UPLOAD_FOLDER')) upload_folder = os.path.join(os.path.normpath(app.root_path), get_config('UPLOAD_FOLDER'))
for root, dirs, files in os.walk(upload_folder): for root, dirs, files in os.walk(upload_folder):
@@ -863,16 +871,23 @@ def import_ctf(backup, segments=None, erase=False):
elif item == 'pages': elif item == 'pages':
saved = json.loads(data) saved = json.loads(data)
for entry in saved['results']: for entry in saved['results']:
# Support migration c12d2a1b0926_add_draft_and_title_to_pages
route = entry['route'] route = entry['route']
title = entry.get('title', route.title())
html = entry['html'] html = entry['html']
draft = entry.get('draft', False)
auth_required = entry.get('auth_required', False)
page = Pages.query.filter_by(route=route).first() page = Pages.query.filter_by(route=route).first()
if page: if page:
page.html = html page.html = html
else: else:
page = Pages(route, html) page = Pages(title, route, html, draft=draft, auth_required=auth_required)
db.session.add(page) db.session.add(page)
db.session.commit() db.session.commit()
teams_base = db.session.query(db.func.max(Teams.id)).scalar() or 0
chals_base = db.session.query(db.func.max(Challenges.id)).scalar() or 0
for segment in segments: for segment in segments:
group = groups[segment] group = groups[segment]
for item in group: for item in group:
@@ -888,11 +903,31 @@ def import_ctf(backup, segments=None, erase=False):
if get_config('SQLALCHEMY_DATABASE_URI').startswith('sqlite'): if get_config('SQLALCHEMY_DATABASE_URI').startswith('sqlite'):
for k, v in entry.items(): for k, v in entry.items():
if isinstance(v, six.string_types): if isinstance(v, six.string_types):
try: match = re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d", v)
if match:
entry[k] = datetime.datetime.strptime(v, '%Y-%m-%dT%H:%M:%S.%f')
continue
match = re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", v)
if match:
entry[k] = datetime.datetime.strptime(v, '%Y-%m-%dT%H:%M:%S') entry[k] = datetime.datetime.strptime(v, '%Y-%m-%dT%H:%M:%S')
except ValueError as e: continue
pass for k, v in entry.items():
table.insert(entry) if k == 'chal' or k == 'chalid':
entry[k] += chals_base
if k == 'team' or k == 'teamid':
entry[k] += teams_base
if item == 'teams':
table.insert_ignore(entry, ['email'])
elif item == 'keys':
# Support migration 2539d8b5082e_rename_key_type_to_type
key_type = entry.get('key_type', None)
if key_type is not None:
entry['type'] = key_type
del entry['key_type']
table.insert(entry)
else:
table.insert(entry)
else: else:
continue continue
@@ -914,6 +949,6 @@ def import_ctf(backup, segments=None, erase=False):
os.makedirs(dirname) os.makedirs(dirname)
source = backup.open(f) source = backup.open(f)
target = file(full_path, "wb") target = open(full_path, "wb")
with source, target: with source, target:
shutil.copyfileobj(source, target) shutil.copyfileobj(source, target)

View File

@@ -3,7 +3,7 @@
from tests.helpers import * from tests.helpers import *
from CTFd.models import ip2long, long2ip from CTFd.models import ip2long, long2ip
from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended, export_ctf from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended, export_ctf, import_ctf
from CTFd.utils import register_plugin_script, register_plugin_stylesheet from CTFd.utils import register_plugin_script, register_plugin_stylesheet
from CTFd.utils import base64encode, base64decode from CTFd.utils import base64encode, base64decode
from CTFd.utils import check_email_format from CTFd.utils import check_email_format
@@ -11,6 +11,7 @@ from CTFd.utils import update_check
from freezegun import freeze_time from freezegun import freeze_time
from mock import patch, Mock from mock import patch, Mock
import json import json
import os
import requests import requests
import six import six
@@ -348,9 +349,49 @@ def test_export_ctf():
output = json.loads(output) output = json.loads(output)
app.db.session.commit() app.db.session.commit()
backup = export_ctf() backup = export_ctf()
backup.seek(0)
with open('export.zip', 'wb') as f: with open('export.zip', 'wb') as f:
f.write(backup.getvalue()) f.write(backup.getvalue())
os.remove('export.zip')
destroy_ctfd(app)
def test_import_ctf():
"""Test that CTFd can import a CTF"""
app = create_ctfd()
# TODO: Unrelated to an in-memory database, imports in a test environment are not working with SQLite...
if app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite') is False:
with app.app_context():
base_user = 'user'
for x in range(10):
user = base_user + str(x)
user_email = user + "@ctfd.io"
gen_team(app.db, name=user, email=user_email)
for x in range(10):
chal = gen_challenge(app.db, name='chal_name{}'.format(x))
gen_flag(app.db, chal=chal.id, flag='flag')
app.db.session.commit()
backup = export_ctf()
with open('export.zip', 'wb') as f:
f.write(backup.read())
destroy_ctfd(app)
app = create_ctfd()
with app.app_context():
import_ctf('export.zip')
app.db.session.commit()
print(Teams.query.count())
print(Challenges.query.count())
assert Teams.query.count() == 11
assert Challenges.query.count() == 10
assert Keys.query.count() == 10
destroy_ctfd(app) destroy_ctfd(app)