From 668abba71952a6c8bfa731031f05387690236529 Mon Sep 17 00:00:00 2001 From: Nicolas FAUGEROUX Date: Mon, 13 Jan 2025 19:46:51 +0100 Subject: [PATCH] WIP: fix(api)!: standard REST API --- api.py | 54 +++++++++++++-------------------- assets/instances.js | 18 ++++++++--- assets/view.js | 6 ++-- hack/docker-compose.yml | 4 +-- models.py | 10 +++--- test/test_api_admin_instance.py | 28 +++++++++++------ test/utils.py | 10 ++++-- 7 files changed, 73 insertions(+), 57 deletions(-) diff --git a/api.py b/api.py index 25a1660..afa6848 100644 --- a/api.py +++ b/api.py @@ -58,10 +58,8 @@ def get(): 'message': f"Error while communicating with CM : {e}", }} - return {'success': True, 'data': { - 'message': json.loads(r.text), - }} - + return {'success': True, 'data': json.loads(r.text)} + @staticmethod @admins_only def post(): @@ -112,16 +110,15 @@ def post(): finally: lock.admin_unlock() - return {'success': True, 'data': { - 'message': json.loads(r.text), - }} + return {'success': True, 'data': json.loads(r.text)} @staticmethod @admins_only def patch(): # mandatory - challengeId = request.args.get("challengeId") - sourceId = request.args.get("sourceId") + data = request.get_json() + challengeId = data.get("challengeId") + sourceId = data.get("sourceId") adminId = str(current_user.get_current_user().id) logger.info(f"Admin {adminId} request instance update for challengeId: {challengeId}, sourceId: {sourceId}") @@ -136,16 +133,15 @@ def patch(): 'message': f"Error while communicating with CM : {e}", }} - return {'success': True, 'data': { - 'message': json.loads(r.text), - }} + return {'success': True, 'data': json.loads(r.text)} @staticmethod @admins_only def delete(): # mandatory - challengeId = request.args.get("challengeId") - sourceId = request.args.get("sourceId") + data = request.get_json() + challengeId = data.get("challengeId") + sourceId = data.get("sourceId") cm_mana_total = get_config("chall-manager:chall-manager_mana_total") @@ -178,9 +174,7 @@ def delete(): finally: lock.admin_unlock() - return {'success': True, 'data': { - 'message': json.loads(r.text), - }} + return {'success': True, 'data': json.loads(r.text)} # region UserInstance @@ -231,9 +225,7 @@ def get(): if 'until' in result.keys(): data['until'] = result['until'] - return {'success': True, 'data': { - 'message': data, - }} + return {'success': True, 'data': data} @staticmethod @authed_only @@ -319,16 +311,15 @@ def post(): if 'until' in result.keys(): data['until'] = result['until'] - return {'success': True, 'data': { - 'message': data, - }} + return {'success': True, 'data': data} @staticmethod @authed_only @challenge_visible def patch(): # mandatory - challengeId = request.args.get("challengeId") + data = request.get_json() + challengeId = data.get("challengeId") # check userMode of CTFd sourceId = str(current_user.get_current_user().id) @@ -359,9 +350,7 @@ def patch(): 'message': f"Error while communicating with CM : {e}", }} - return {'success': True, 'data': { - 'message': f"{r.status_code}", - }} + return {'success': True, 'data': {}} @staticmethod @authed_only @@ -370,7 +359,8 @@ def delete(): # retrieve all instances deployed by chall-manager cm_mana_total = get_config("chall-manager:chall-manager_mana_total") - challengeId = request.args.get("challengeId") + data = request.get_json() + challengeId = data.get("challengeId") # check userMode of CTFd sourceId = str(current_user.get_current_user().id) @@ -412,9 +402,7 @@ def delete(): delete_coupon(challengeId, sourceId) logger.info(f"Coupon deleted for challengeId: {challengeId}, sourceId: {sourceId}") - return {'success': True, 'data': { - 'message': json.loads(r.text), - }} + return {'success': True, 'data': {}} # region UserMana @@ -442,7 +430,7 @@ def get(): lock.player_unlock() return {'success': True, 'data': { - 'mana_used': f"{mana}", - 'mana_total': f"{get_config('chall-manager:chall-manager_mana_total')}", + 'used': f"{mana}", + 'total': f"{get_config('chall-manager:chall-manager_mana_total')}", }} diff --git a/assets/instances.js b/assets/instances.js index a58f02f..945af85 100644 --- a/assets/instances.js +++ b/assets/instances.js @@ -1,11 +1,16 @@ async function delete_instance(challengeId, sourceId) { - let response = await CTFd.fetch("/api/v1/plugins/ctfd-chall-manager/admin/instance?challengeId=" + challengeId + "&sourceId=" + sourceId, { + let params = { + "challengeId": challengeId.toString(), + "sourceId": sourceId.toString() + }; + let response = await CTFd.fetch("/api/v1/plugins/ctfd-chall-manager/admin/instance" , { method: "DELETE", credentials: "same-origin", headers: { "Accept": "application/json", "Content-Type": "application/json" - } + }, + body: JSON.stringify(params) }); console.log(response) response = await response.json(); @@ -13,13 +18,18 @@ async function delete_instance(challengeId, sourceId) { } async function renew_instance(challengeId, sourceId) { - let response = await CTFd.fetch("/api/v1/plugins/ctfd-chall-manager/admin/instance?challengeId=" + challengeId + "&sourceId=" + sourceId, { + let params = { + "challengeId": challengeId.toString(), + "sourceId": sourceId.toString() + }; + let response = await CTFd.fetch("/api/v1/plugins/ctfd-chall-manager/admin/instance", { method: "PATCH", credentials: "same-origin", headers: { "Accept": "application/json", "Content-Type": "application/json" - } + }, + body: JSON.stringify(params) }); console.log(response) response = await response.json(); diff --git a/assets/view.js b/assets/view.js index 1b08af1..a370a4b 100644 --- a/assets/view.js +++ b/assets/view.js @@ -205,12 +205,14 @@ CTFd._internal.challenge.destroy = function() { CTFd._internal.challenge.renew = function () { var challenge_id = CTFd._internal.challenge.data.id; - var url = "/api/v1/plugins/ctfd-chall-manager/instance?challengeId=" + challenge_id; + var url = "/api/v1/plugins/ctfd-chall-manager/instance"; $('#whale-button-renew').text("Waiting..."); $('#whale-button-renew').prop('disabled', true); - var params = {}; + var params = { + "challengeId": challenge_id, + }; CTFd.fetch(url, { method: 'PATCH', diff --git a/hack/docker-compose.yml b/hack/docker-compose.yml index 8e3e9e8..6b2a7aa 100644 --- a/hack/docker-compose.yml +++ b/hack/docker-compose.yml @@ -11,8 +11,8 @@ services: LOG_LEVEL: DEBUG PLUGIN_SETTINGS_CM_API_URL: http://chall-manager:8080/api/v1 PLUGIN_SETTINGS_CM_MANA_TOTAL: 10 - REDIS_URL: redis://redis-svc:6379 - DATABASE_URL : mysql+pymysql://root:password@mariadb-svc:3306/ctfd + # REDIS_URL: redis://redis-svc:6379 + # DATABASE_URL : mysql+pymysql://root:password@mariadb-svc:3306/ctfd depends_on: - chall-manager # - redis-svc diff --git a/models.py b/models.py index 3d118d2..3cf84f6 100644 --- a/models.py +++ b/models.py @@ -49,7 +49,7 @@ def __str__(self): -class DynamicIaCValueChallenge(BaseChallenge): +class DynamicIaCValueChallenge(DynamicValueChallenge): id = "dynamic_iac" # Unique identifier used to register challenges name = "dynamic_iac" # Name of a challenge type templates = { # Handlebars templates used for each aspect of challenge editing & viewing @@ -214,7 +214,7 @@ def update(cls, challenge, request): # Workaround if "state" in data.keys() and len(data.keys()) == 1: setattr(challenge, "state", data["state"]) - return DynamicValueChallenge.calculate_value(challenge) + return super().calculate_value(challenge) # Patch Challenge on CTFd optional = {} @@ -267,7 +267,7 @@ def update(cls, challenge, request): except Exception as e: logger.error(f"Error while patching the challenge: {e}") - return DynamicValueChallenge.calculate_value(challenge) + return super().calculate_value(challenge) @classmethod def delete(cls, challenge): @@ -392,8 +392,8 @@ def attempt(cls, challenge, request): logger.info(f"invalid submission for CTFd flag: challenge {challenge.id} source {sourceId}") return False, "Incorrect" + # remove this ? @classmethod def solve(cls, user, team, challenge, request): super().solve(user, team, challenge, request) - - DynamicValueChallenge.calculate_value(challenge) + super().calculate_value(challenge) diff --git a/test/test_api_admin_instance.py b/test/test_api_admin_instance.py index fa2181a..26c22fc 100644 --- a/test/test_api_admin_instance.py +++ b/test/test_api_admin_instance.py @@ -33,16 +33,16 @@ def test_valid_challenge_valid_source(self): a = json.loads(r.text) self.assertEqual(a["success"], True) - r = requests.get(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.get(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], True) self.assertEqual("connectionInfo" in a["data"]["message"].keys(), True) - r = requests.patch(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.patch(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], True) - r = requests.delete(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.delete(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], True) @@ -64,11 +64,11 @@ def test_invalid_challenge_valid_source(self): a = json.loads(r.text) self.assertEqual(a["success"], False) - r = requests.patch(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.patch(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], False) - r = requests.delete(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.delete(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], False) @@ -88,11 +88,11 @@ def test_valid_challenge_unknown_source(self): a = json.loads(r.text) self.assertEqual(a["success"], True) - r = requests.patch(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.patch(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], True) - r = requests.delete(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + r = requests.delete(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], True) @@ -101,7 +101,12 @@ def test_valid_challenge_unknown_source(self): def test_delete_valid_challenge_but_no_instance(self): challengeId = create_challenge() sourceId = 999999 - r = requests.delete(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + payload = { + "challengeId": f"{challengeId}", + "sourceId": f"{sourceId}" + } + + r = requests.delete(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], False) delete_challenge(challengeId) @@ -109,7 +114,12 @@ def test_delete_valid_challenge_but_no_instance(self): def test_patch_valid_challenge_but_no_instance(self): challengeId = create_challenge(timeout=9999) sourceId = 999999 - r = requests.patch(f"{config.plugin_url}/admin/instance?challengeId={challengeId}&sourceId={sourceId}", headers=config.headers_admin) + payload = { + "challengeId": f"{challengeId}", + "sourceId": f"{sourceId}" + } + + r = requests.patch(f"{config.plugin_url}/admin/instance", headers=config.headers_admin, data=json.dumps(payload)) a = json.loads(r.text) self.assertEqual(a["success"], False) diff --git a/test/utils.py b/test/utils.py index 73868dd..a8d5fcf 100644 --- a/test/utils.py +++ b/test/utils.py @@ -108,11 +108,17 @@ def get_admin_instance(challengeId: int, sourceId: int): return r def delete_instance(challengeId: int): - r = requests.delete(f"{config.plugin_url}/instance?challengeId={challengeId}", headers=config.headers_user) + payload = { + "challengeId": f"{challengeId}" + } + r = requests.delete(f"{config.plugin_url}/instance", headers=config.headers_user, data=json.dumps(payload)) return r def patch_instance(challengeId: int): - r = requests.patch(f"{config.plugin_url}/instance?challengeId={challengeId}", headers=config.headers_user) + payload = { + "challengeId": f"{challengeId}" + } + r = requests.patch(f"{config.plugin_url}/instance", headers=config.headers_user, data=json.dumps(payload)) return r # Run post on thread