mirror of https://github.com/JohnHammond/CTFd.git
Custom challenge tables (#425)
* Allows Challenges to have custom tables which allow them to have custom behavior * Adding create, update, delete staticmethods giving Challenge Types a static interface to implementselenium-screenshot-testing
parent
cdc7a70fe7
commit
710ce6d500
|
@ -73,6 +73,18 @@ def admin_chals():
|
|||
return render_template('admin/chals.html')
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chals/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_chal_detail(chalid):
|
||||
if request.method == 'POST':
|
||||
pass
|
||||
elif request.method == 'GET':
|
||||
chal = Challenges.query.filter_by(id=chalid).first_or_404()
|
||||
chal_class = get_chal_class(chal.type)
|
||||
obj, data = chal_class.read(chal)
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_tags(chalid):
|
||||
|
@ -236,41 +248,9 @@ def admin_get_values(chalid, prop):
|
|||
@admins_only
|
||||
def admin_create_chal():
|
||||
if request.method == 'POST':
|
||||
files = request.files.getlist('files[]')
|
||||
|
||||
# Create challenge
|
||||
chal = Challenges(
|
||||
name=request.form['name'],
|
||||
description=request.form['desc'],
|
||||
value=request.form['value'],
|
||||
category=request.form['category'],
|
||||
type=request.form['chaltype']
|
||||
)
|
||||
|
||||
if 'hidden' in request.form:
|
||||
chal.hidden = True
|
||||
else:
|
||||
chal.hidden = False
|
||||
|
||||
max_attempts = request.form.get('max_attempts')
|
||||
if max_attempts and max_attempts.isdigit():
|
||||
chal.max_attempts = int(max_attempts)
|
||||
|
||||
db.session.add(chal)
|
||||
db.session.flush()
|
||||
|
||||
flag = Keys(chal.id, request.form['key'], request.form['key_type[0]'])
|
||||
if request.form.get('keydata'):
|
||||
flag.data = request.form.get('keydata')
|
||||
db.session.add(flag)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
for f in files:
|
||||
utils.upload_file(file=f, chalid=chal.id)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
chal_type = request.form['chaltype']
|
||||
chal_class = get_chal_class(chal_type)
|
||||
chal_class.create(request)
|
||||
return redirect(url_for('admin_challenges.admin_chals'))
|
||||
else:
|
||||
return render_template('admin/chals/create.html')
|
||||
|
@ -280,17 +260,8 @@ def admin_create_chal():
|
|||
@admins_only
|
||||
def admin_delete_chal():
|
||||
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
|
||||
WrongKeys.query.filter_by(chalid=challenge.id).delete()
|
||||
Solves.query.filter_by(chalid=challenge.id).delete()
|
||||
Keys.query.filter_by(chal=challenge.id).delete()
|
||||
files = Files.query.filter_by(chal=challenge.id).all()
|
||||
for f in files:
|
||||
utils.delete_file(f.id)
|
||||
Files.query.filter_by(chal=challenge.id).delete()
|
||||
Tags.query.filter_by(chal=challenge.id).delete()
|
||||
Challenges.query.filter_by(id=challenge.id).delete()
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
chal_class.delete(challenge)
|
||||
return '1'
|
||||
|
||||
|
||||
|
@ -298,13 +269,6 @@ def admin_delete_chal():
|
|||
@admins_only
|
||||
def admin_update_chal():
|
||||
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
|
||||
challenge.name = request.form['name']
|
||||
challenge.description = request.form['desc']
|
||||
challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0
|
||||
challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0
|
||||
challenge.category = request.form['category']
|
||||
challenge.hidden = 'hidden' in request.form
|
||||
db.session.add(challenge)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
chal_class.update(challenge, request)
|
||||
return redirect(url_for('admin_challenges.admin_chals'))
|
||||
|
|
|
@ -50,6 +50,10 @@ class Challenges(db.Model):
|
|||
category = db.Column(db.String(80))
|
||||
type = db.Column(db.String(80))
|
||||
hidden = db.Column(db.Boolean)
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'standard',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __init__(self, name, description, value, category, type='standard'):
|
||||
self.name = name
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from CTFd.plugins import register_plugin_assets_directory
|
||||
from CTFd.plugins.keys import get_key_class
|
||||
from CTFd.models import db, Solves, WrongKeys, Keys
|
||||
from CTFd.models import db, Solves, WrongKeys, Keys, Challenges, Files, Tags
|
||||
from CTFd import utils
|
||||
|
||||
|
||||
|
@ -25,6 +25,113 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
'modal': '/plugins/challenges/assets/standard-challenge-modal.js',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create(request):
|
||||
"""
|
||||
This method is used to process the challenge creation request.
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
files = request.files.getlist('files[]')
|
||||
|
||||
# Create challenge
|
||||
chal = Challenges(
|
||||
name=request.form['name'],
|
||||
description=request.form['desc'],
|
||||
value=request.form['value'],
|
||||
category=request.form['category'],
|
||||
type=request.form['chaltype']
|
||||
)
|
||||
|
||||
if 'hidden' in request.form:
|
||||
chal.hidden = True
|
||||
else:
|
||||
chal.hidden = False
|
||||
|
||||
max_attempts = request.form.get('max_attempts')
|
||||
if max_attempts and max_attempts.isdigit():
|
||||
chal.max_attempts = int(max_attempts)
|
||||
|
||||
db.session.add(chal)
|
||||
db.session.commit()
|
||||
|
||||
flag = Keys(chal.id, request.form['key'], request.form['key_type[0]'])
|
||||
if request.form.get('keydata'):
|
||||
flag.data = request.form.get('keydata')
|
||||
db.session.add(flag)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
for f in files:
|
||||
utils.upload_file(file=f, chalid=chal.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def read(challenge):
|
||||
"""
|
||||
This method is in used to access the data of a challenge in a format processable by the front end.
|
||||
|
||||
:param challenge:
|
||||
:return: Challenge object, data dictionary to be returned to the user
|
||||
"""
|
||||
data = {
|
||||
'id': challenge.id,
|
||||
'name': challenge.name,
|
||||
'value': challenge.value,
|
||||
'description': challenge.description,
|
||||
'category': challenge.category,
|
||||
'hidden': challenge.hidden,
|
||||
'max_attempts': challenge.max_attempts,
|
||||
'type': challenge.type,
|
||||
'type_data': {
|
||||
'id': CTFdStandardChallenge.id,
|
||||
'name': CTFdStandardChallenge.name,
|
||||
'templates': CTFdStandardChallenge.templates,
|
||||
'scripts': CTFdStandardChallenge.scripts,
|
||||
}
|
||||
}
|
||||
return challenge, data
|
||||
|
||||
@staticmethod
|
||||
def update(challenge, request):
|
||||
"""
|
||||
This method is used to update the information associated with a challenge. This should be kept strictly to the
|
||||
Challenges table and any child tables.
|
||||
|
||||
:param challenge:
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
challenge.name = request.form['name']
|
||||
challenge.description = request.form['desc']
|
||||
challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0
|
||||
challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0
|
||||
challenge.category = request.form['category']
|
||||
challenge.hidden = 'hidden' in request.form
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
@staticmethod
|
||||
def delete(challenge):
|
||||
"""
|
||||
This method is used to delete the resources used by a challenge.
|
||||
|
||||
:param challenge:
|
||||
:return:
|
||||
"""
|
||||
WrongKeys.query.filter_by(chalid=challenge.id).delete()
|
||||
Solves.query.filter_by(chalid=challenge.id).delete()
|
||||
Keys.query.filter_by(chal=challenge.id).delete()
|
||||
files = Files.query.filter_by(chal=challenge.id).all()
|
||||
for f in files:
|
||||
utils.delete_file(f.id)
|
||||
Files.query.filter_by(chal=challenge.id).delete()
|
||||
Tags.query.filter_by(chal=challenge.id).delete()
|
||||
Challenges.query.filter_by(id=challenge.id).delete()
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def attempt(chal, request):
|
||||
"""
|
||||
|
|
|
@ -354,32 +354,29 @@ $('#hint-modal-submit').submit(function (e) {
|
|||
});
|
||||
|
||||
function loadchal(id, update) {
|
||||
// $('#chal *').show()
|
||||
// $('#chal > h1').hide()
|
||||
obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0]
|
||||
$('#desc-write-link').click() // Switch to Write tab
|
||||
$('.chal-title').text(obj.name);
|
||||
$('.chal-name').val(obj.name);
|
||||
$('.chal-desc').val(obj.description);
|
||||
$('.chal-value').val(obj.value);
|
||||
if (parseInt(obj.max_attempts) > 0){
|
||||
$('.chal-attempts').val(obj.max_attempts);
|
||||
$('#limit_max_attempts').prop('checked', true);
|
||||
$('#chal-attempts-group').show();
|
||||
$.get(script_root + '/admin/chals/' + id, function(obj){
|
||||
$('#desc-write-link').click() // Switch to Write tab
|
||||
$('.chal-title').text(obj.name);
|
||||
$('.chal-name').val(obj.name);
|
||||
$('.chal-desc').val(obj.description);
|
||||
$('.chal-value').val(obj.value);
|
||||
if (parseInt(obj.max_attempts) > 0){
|
||||
$('.chal-attempts').val(obj.max_attempts);
|
||||
$('#limit_max_attempts').prop('checked', true);
|
||||
$('#chal-attempts-group').show();
|
||||
}
|
||||
$('.chal-category').val(obj.category);
|
||||
$('.chal-id').val(obj.id);
|
||||
$('.chal-hidden').prop('checked', false);
|
||||
if (obj.hidden) {
|
||||
$('.chal-hidden').prop('checked', true);
|
||||
}
|
||||
//$('#update-challenge .chal-delete').attr({
|
||||
// 'href': '/admin/chal/close/' + (id + 1)
|
||||
//})
|
||||
if (typeof update === 'undefined')
|
||||
$('#update-challenge').modal();
|
||||
}
|
||||
$('.chal-category').val(obj.category);
|
||||
$('.chal-id').val(obj.id);
|
||||
$('.chal-hidden').prop('checked', false);
|
||||
if (obj.hidden) {
|
||||
$('.chal-hidden').prop('checked', true);
|
||||
}
|
||||
//$('#update-challenge .chal-delete').attr({
|
||||
// 'href': '/admin/chal/close/' + (id + 1)
|
||||
//})
|
||||
if (typeof update === 'undefined')
|
||||
$('#update-challenge').modal();
|
||||
}
|
||||
|
||||
function openchal(id){
|
||||
|
|
|
@ -40,9 +40,9 @@ function load_edit_key_modal(key_id, key_type_name) {
|
|||
}
|
||||
|
||||
function load_chal_template(id, success_cb){
|
||||
obj = $.grep(challenges['game'], function (e) {
|
||||
var obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0]
|
||||
})[0];
|
||||
$.get(script_root + obj.type_data.templates.update, function(template_data){
|
||||
var template = Handlebars.compile(template_data);
|
||||
$("#update-modals-entry-div").html(template({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
||||
|
@ -50,7 +50,7 @@ function load_chal_template(id, success_cb){
|
|||
url: script_root + obj.type_data.scripts.update,
|
||||
dataType: "script",
|
||||
success: success_cb,
|
||||
cache: true,
|
||||
cache: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ function loadchals(){
|
|||
};
|
||||
|
||||
for (var i = 0; i <= challenges['game'].length - 1; i++) {
|
||||
var chal = challenges['game'][i]
|
||||
var chal = challenges['game'][i];
|
||||
var chal_button = $('<button class="chal-button col-md-2 theme-background" value="{0}"><h5>{1}</h5><p class="chal-points">{2}</p><span class="chal-percent">{3}% solved</span></button>'.format(chal.id, chal.name, chal.value, Math.round(chal.percentage_solved * 100)));
|
||||
$('#' + challenges['game'][i].category.replace(/ /g,"-").hashCode()).append(chal_button);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import *
|
||||
from CTFd.models import Teams
|
||||
from CTFd.models import Teams, Challenges
|
||||
from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from freezegun import freeze_time
|
||||
from mock import patch
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def test_admin_panel():
|
||||
"""Does the admin panel return a 200 by default"""
|
||||
|
@ -103,3 +109,115 @@ def test_admins_can_access_challenges_before_ctftime():
|
|||
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
||||
assert solve_count == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admins_can_create_challenges():
|
||||
'''Test that admins can create new challenges'''
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
'name': '💫',
|
||||
'category': '💫',
|
||||
'desc': 'description',
|
||||
'value': 100,
|
||||
'key_type[0]': 'static',
|
||||
'max_attempts': '',
|
||||
'nonce': sess.get('nonce'),
|
||||
'chaltype': 'standard'
|
||||
}
|
||||
r = client.post('/admin/chal/new', data=data)
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admins_can_update_challenges():
|
||||
'''Test that admins can update challenges'''
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
'id': chal_id,
|
||||
'name': '💫',
|
||||
'category': '💫',
|
||||
'desc': 'description',
|
||||
'value': 100,
|
||||
'key_type[0]': 'static',
|
||||
'max_attempts': '',
|
||||
'nonce': sess.get('nonce'),
|
||||
'chaltype': 'standard'
|
||||
}
|
||||
r = client.post('/admin/chal/update', data=data)
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
chal_check = Challenges.query.filter_by(id=chal_id).first()
|
||||
assert chal_check.name == '💫'
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admins_can_delete_challenges():
|
||||
'''Test that admins can delete challenges'''
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
'id': chal_id,
|
||||
'nonce': sess.get('nonce'),
|
||||
}
|
||||
r = client.post('/admin/chal/delete', data=data)
|
||||
assert r.get_data(as_text=True) == '1'
|
||||
|
||||
assert Challenges.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_chal_detail_returns_proper_data():
|
||||
"""Test that the /admin/chals/<int:chalid> endpoint returns the proper data"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
chal_class = get_chal_class(chal.type)
|
||||
data = {
|
||||
'id': chal.id,
|
||||
'name': chal.name,
|
||||
'value': chal.value,
|
||||
'description': chal.description,
|
||||
'category': chal.category,
|
||||
'hidden': chal.hidden,
|
||||
'max_attempts': chal.max_attempts,
|
||||
'type': chal.type,
|
||||
'type_data': {
|
||||
'id': chal_class.id,
|
||||
'name': chal_class.name,
|
||||
'templates': chal_class.templates,
|
||||
'scripts': chal_class.scripts,
|
||||
}
|
||||
}
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
|
||||
r = client.get('/admin/chals/1')
|
||||
response = json.loads(r.get_data(as_text=True))
|
||||
|
||||
assert data == response
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
|
Loading…
Reference in New Issue