Repository: CTFd/CTFd

Issue:
2.0.0: Admin page and Dynamic challenges' delete function error. 
**Environment**:

  - CTFd Version/Commit: **Docker ctfd/ctfd:finalize-2.0.0** and **mark-2.0.0**
  - Operating System: **Ubuntu14.04-Server**
  - Web Browser and Version:  **Chrome 70.0.3538.102 / Microsoft EdgeHTML 17.17134**

**What happened?**
I tried to delete the dynamic challenge. After confirming in the popup box, it does not take effect.   (Standard challenge is ok.)
Then I re-created a docker:mark-2.0.0 environment and found that I could not enter the admin page. 
The terminal's error message is below.

**What did you expect to happen?**

**How to reproduce your issue**
```bash
sudo docker run -p 8000:8000 -itd ctfd/ctfd:finalize-2.0.0
```

**Any associated stack traces or error logs**
![111.png](https://i.loli.net/2018/11/26/5bfc03d7cf5a3.png)
```
154.85.36.216 - - [26/Nov/2018:15:02:10 +0000] "DELETE /api/v1/challenges/1 HTTP/1.1" 500 37 "http://66.42.56.190:9999/admin/challenges/1" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
[2018-11-26 15:02:13,740] ERROR in app: Exception on /api/v1/challenges/1 [DELETE]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python2.7/site-packages/flask_restplus/api.py", line 325, in wrapper
    resp = resource(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/flask/views.py", line 88, in view
    return self.dispatch_request(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/flask_restplus/resource.py", line 44, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/opt/CTFd/CTFd/utils/decorators/__init__.py", line 98, in admins_only_wrapper
    return f(*args, **kwargs)
  File "/opt/CTFd/CTFd/api/v1/challenges.py", line 255, in delete
    chal_class.delete(challenge)
  File "/opt/CTFd/CTFd/plugins/dynamic_challenges/__init__.py", line 125, in delete
    files = Files.query.filter_by(challenge_id=challenge.id).all()
  File "/usr/local/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 1689, in filter_by
    for key, value in kwargs.items()]
  File "/usr/local/lib/python2.7/site-packages/sqlalchemy/orm/base.py", line 383, in _entity_descriptor
    (description, key)
InvalidRequestError: Entity '<class 'CTFd.models.Files'>' has no property 'challenge_id'
```
```
154.85.36.216 - - [26/Nov/2018:14:44:35 +0000] "GET /admin/statistics HTTP/1.1" 500 4144 "http://66.42.56.190:9999/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
154.85.36.216 - - [26/Nov/2018:14:44:35 +0000] "GET /socket.io/?EIO=3&transport=polling&t=1543243473248-5 HTTP/1.1" 404 4195 "http://66.42.56.190:9999/admin/statistics" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
154.85.36.216 - - [26/Nov/2018:14:44:35 +0000] "GET /static/user.css HTTP/1.1" 200 0 "http://66.42.56.190:9999/admin/statistics" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
154.85.36.216 - - [26/Nov/2018:14:44:36 +0000] "GET /socket.io/?EIO=3&transport=polling&t=1543243473963-0 HTTP/1.1" 404 4195 "http://66.42.56.190:9999/admin/statistics" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
154.85.36.216 - - [26/Nov/2018:14:44:36 +0000] "GET /themes/core/static/img/favicon.ico HTTP/1.1" 200 0 "http://66.42.56.190:9999/admin/statistics" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
154.85.36.216 - - [26/Nov/2018:14:44:37 +0000] "GET /socket.io/?EIO=3&transport=polling&t=1543243474738-1 HTTP/1.1" 404 4195 "http://66.42.56.190:9999/admin/statistics" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
[2018-11-26 14:44:38,404] ERROR in app: Exception on /admin/statistics [GET]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/site-packages/flask_restplus/api.py", line 583, in error_router
    return original_handler(e)
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/CTFd/CTFd/utils/decorators/__init__.py", line 98, in admins_only_wrapper
    return f(*args, **kwargs)
  File "/opt/CTFd/CTFd/admin/statistics.py", line 12, in statistics
    update_check()
  File "/opt/CTFd/CTFd/utils/updates/__init__.py", line 41, in update_check
    'ctf_id': sha256(name),
  File "/opt/CTFd/CTFd/utils/security/passwords.py", line 17, in sha256
    return hashlib.sha256(p).hexdigest()
TypeError: sha256() argument 1 must be string or buffer, not int
```

Many of my deployments have failed, and it seems that docker's deployment solution is not stable.


Discussion:

Resolving patch:
diff --git a/CTFd/plugins/dynamic_challenges/__init__.py b/CTFd/plugins/dynamic_challenges/__init__.py
--- a/CTFd/plugins/dynamic_challenges/__init__.py
+++ b/CTFd/plugins/dynamic_challenges/__init__.py
@@ -2,7 +2,7 @@
 from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
 from CTFd.plugins import register_plugin_assets_directory
 from CTFd.plugins.flags import get_flag_class
-from CTFd.models import db, Solves, Fails, Flags, Challenges, Files, Tags, Teams, Hints
+from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Teams, Hints
 from CTFd import utils
 from CTFd.utils.migrations import upgrade
 from CTFd.utils.user import get_ip
@@ -122,10 +122,10 @@ def delete(challenge):
         Fails.query.filter_by(challenge_id=challenge.id).delete()
         Solves.query.filter_by(challenge_id=challenge.id).delete()
         Flags.query.filter_by(challenge_id=challenge.id).delete()
-        files = Files.query.filter_by(challenge_id=challenge.id).all()
+        files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all()
         for f in files:
             delete_file(f.id)
-        Files.query.filter_by(challenge_id=challenge.id).delete()
+        ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete()
         Tags.query.filter_by(challenge_id=challenge.id).delete()
         Hints.query.filter_by(challenge_id=challenge.id).delete()
         DynamicChallenge.query.filter_by(id=challenge.id).delete()
diff --git a/CTFd/utils/updates/__init__.py b/CTFd/utils/updates/__init__.py
--- a/CTFd/utils/updates/__init__.py
+++ b/CTFd/utils/updates/__init__.py
@@ -36,7 +36,7 @@ def update_check(force=False):
 
     if update:
         try:
-            name = get_config('ctf_name') or ''
+            name = str(get_config('ctf_name')) or ''
             params = {
                 'ctf_id': sha256(name),
                 'current': app.VERSION,
diff --git a/migrations/1_2_0_upgrade_2_0_0.py b/migrations/1_2_0_upgrade_2_0_0.py
--- a/migrations/1_2_0_upgrade_2_0_0.py
+++ b/migrations/1_2_0_upgrade_2_0_0.py
@@ -129,7 +129,7 @@
 
     submissions = []
     for solve in old_data['solves']:
-        solve.pop('id') # ID of a solve doesn't really matter
+        solve.pop('id')  # ID of a solve doesn't really matter
         solve['challenge_id'] = solve.pop('chalid')
         solve['user_id'] = solve.pop('teamid')
         solve['provided'] = solve.pop('flag')
@@ -138,7 +138,7 @@
         submissions.append(solve)
 
     for wrong_key in old_data['wrong_keys']:
-        wrong_key.pop('id') # ID of a fail doesn't really matter.
+        wrong_key.pop('id')  # ID of a fail doesn't really matter.
         wrong_key['challenge_id'] = wrong_key.pop('chalid')
         wrong_key['user_id'] = wrong_key.pop('teamid')
         wrong_key['provided'] = wrong_key.pop('flag')
@@ -168,16 +168,70 @@
     print('MIGRATING Config')
     banned = [
         'ctf_version',
-        'setup'
     ]
+    workshop_mode = None
+    hide_scores = None
+    prevent_registration = None
+    view_challenges_unregistered = None
+    view_scoreboard_if_authed = None
+
+    challenge_visibility = 'private'
+    registration_visibility = 'public'
+    score_visibility = 'public'
+    account_visibility = 'public'
     for config in old_data['config']:
         config.pop('id')
+
+        if config['key'] == 'workshop_mode':
+            workshop_mode = config['value']
+        elif config['key'] == 'hide_scores':
+            hide_scores = config['value']
+        elif config['key'] == 'prevent_registration':
+            prevent_registration = config['value']
+        elif config['key'] == 'view_challenges_unregistered':
+            view_challenges_unregistered = config['value']
+        elif config['key'] == 'view_scoreboard_if_authed':
+            view_scoreboard_if_authed = config['value']
+
         if config['key'] not in banned:
             new_conn['config'].insert(dict(config))
+
+    if workshop_mode:
+        score_visibility = 'admins'
+        account_visibility = 'admins'
+
+    if hide_scores:
+        score_visibility = 'hidden'
+
+    if prevent_registration:
+        registration_visibility = 'private'
+
+    if view_challenges_unregistered:
+        challenge_visibility = 'public'
+
+    if view_scoreboard_if_authed:
+        score_visibility = 'private'
+
     new_conn['config'].insert({
         'key': 'user_mode',
         'value': 'users'
     })
+    new_conn['config'].insert({
+        'key': 'challenge_visibility',
+        'value': challenge_visibility
+    })
+    new_conn['config'].insert({
+        'key': 'registration_visibility',
+        'value': registration_visibility
+    })
+    new_conn['config'].insert({
+        'key': 'score_visibility',
+        'value': score_visibility
+    })
+    new_conn['config'].insert({
+        'key': 'account_visibility',
+        'value': account_visibility
+    })
     del old_data['config']
 
     manual = []
@@ -192,7 +246,7 @@
         for row in data:
             new_conn[table].insert(dict(row))
             ran = True
-        else: # We finished inserting
+        else:  # We finished inserting
             if ran:
                 manual.append(table)
 
@@ -208,6 +262,7 @@
     for table in manual:
         print('\t', 'ALTER TABLE `{table}` ADD PRIMARY KEY(id)'.format(table=table))
 
-    print('The following tables were not created because they were empty and must be manually recreated (e.g. app.db.create_all()')
+    print(
+        'The following tables were not created because they were empty and must be manually recreated (e.g. app.db.create_all()')
     for table in not_created:
         print('\t', table)


Test patch:
diff --git a/tests/challenges/__init__.py b/tests/challenges/__init__.py
new file mode 100644
diff --git a/tests/challenges/test_dynamic.py b/tests/challenges/test_dynamic.py
new file mode 100644
--- /dev/null
+++ b/tests/challenges/test_dynamic.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from CTFd.plugins.dynamic_challenges import DynamicChallenge, DynamicValueChallenge
+from tests.helpers import *
+
+
+def test_can_create_dynamic_challenge():
+    """Test that dynamic challenges can be made from the API/admin panel"""
+    app = create_ctfd(enable_plugins=True)
+    with app.app_context():
+        register_user(app)
+        client = login_as_user(app, name="admin", password="password")
+
+        challenge_data = {
+            "name": "name",
+            "category": "category",
+            "description": "description",
+            "value": 100,
+            "decay": 20,
+            "minimum": 1,
+            "state": "hidden",
+            "type": "dynamic"
+        }
+
+        r = client.post('/api/v1/challenges', json=challenge_data)
+        assert r.get_json().get('data')['id'] == 1
+
+        challenges = DynamicChallenge.query.all()
+        assert len(challenges) == 1
+
+        challenge = challenges[0]
+        assert challenge.value == 100
+        assert challenge.initial == 100
+        assert challenge.decay == 20
+        assert challenge.minimum == 1
+    destroy_ctfd(app)
+
+
+def test_can_update_dynamic_challenge():
+    """Test that dynamic challenges can be deleted"""
+    app = create_ctfd(enable_plugins=True)
+    with app.app_context():
+        challenge_data = {
+            "name": "name",
+            "category": "category",
+            "description": "description",
+            "value": 100,
+            "decay": 20,
+            "minimum": 1,
+            "state": "hidden",
+            "type": "dynamic"
+        }
+        req = FakeRequest(form=challenge_data)
+        challenge = DynamicValueChallenge.create(req)
+
+        assert challenge.value == 100
+        assert challenge.initial == 100
+        assert challenge.decay == 20
+        assert challenge.minimum == 1
+
+        challenge_data = {
+            "name": "new_name",
+            "category": "category",
+            "description": "new_description",
+            "value": "200",
+            "initial": "200",
+            "decay": "40",
+            "minimum": "5",
+            "max_attempts": "0",
+            "state": "visible"
+        }
+
+        req = FakeRequest(form=challenge_data)
+        challenge = DynamicValueChallenge.update(challenge, req)
+
+        assert challenge.name == 'new_name'
+        assert challenge.description == "new_description"
+        assert challenge.value == 200
+        assert challenge.initial == 200
+        assert challenge.decay == 40
+        assert challenge.minimum == 5
+        assert challenge.state == "visible"
+
+    destroy_ctfd(app)
+
+
+def test_can_delete_dynamic_challenge():
+    app = create_ctfd(enable_plugins=True)
+    with app.app_context():
+        register_user(app)
+        client = login_as_user(app, name="admin", password="password")
+
+        challenge_data = {
+            "name": "name",
+            "category": "category",
+            "description": "description",
+            "value": 100,
+            "decay": 20,
+            "minimum": 1,
+            "state": "hidden",
+            "type": "dynamic"
+        }
+
+        r = client.post('/api/v1/challenges', json=challenge_data)
+        assert r.get_json().get('data')['id'] == 1
+
+        challenges = DynamicChallenge.query.all()
+        assert len(challenges) == 1
+
+        challenge = challenges[0]
+        DynamicValueChallenge.delete(challenge)
+
+        challenges = DynamicChallenge.query.all()
+        assert len(challenges) == 0
+    destroy_ctfd(app)
diff --git a/tests/helpers.py b/tests/helpers.py
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -4,6 +4,7 @@
 from CTFd.cache import cache
 from sqlalchemy_utils import database_exists, create_database, drop_database
 from sqlalchemy.engine.url import make_url
+from collections import namedtuple
 import datetime
 import six
 import gc
@@ -16,6 +17,9 @@
     binary_type = bytes
 
 
+FakeRequest = namedtuple('FakeRequest', ['form'])
+
+
 def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="password", user_mode="users", setup=True, enable_plugins=False):
     if enable_plugins:
         TestingConfig.SAFE_MODE = False