From cb8ea7175175a268dc9893309c2b79901c976573 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Fri, 16 Jun 2023 16:27:31 -0400 Subject: [PATCH] Make free hints visible to unauth users if challenges are visible to unauth users --- CTFd/api/v1/hints.py | 23 +++++++-- CTFd/models/__init__.py | 6 +++ tests/api/v1/user/test_hints.py | 88 +++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/CTFd/api/v1/hints.py b/CTFd/api/v1/hints.py index 0ee60fa3..8d347fd9 100644 --- a/CTFd/api/v1/hints.py +++ b/CTFd/api/v1/hints.py @@ -9,7 +9,8 @@ from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessRespon from CTFd.constants import RawEnum from CTFd.models import Hints, HintUnlocks, db from CTFd.schemas.hints import HintSchema -from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only +from CTFd.utils.decorators import admins_only, during_ctf_time_only +from CTFd.utils.decorators.visibility import check_challenge_visibility from CTFd.utils.helpers.models import build_model_filters from CTFd.utils.user import get_current_user, is_admin @@ -105,7 +106,7 @@ class HintList(Resource): @hints_namespace.route("/") class Hint(Resource): @during_ctf_time_only - @authed_only + @check_challenge_visibility @hints_namespace.doc( description="Endpoint to get a specific Hint object", responses={ @@ -117,11 +118,23 @@ class Hint(Resource): }, ) def get(self, hint_id): - user = get_current_user() hint = Hints.query.filter_by(id=hint_id).first_or_404() + user = get_current_user() - if hint.requirements: - requirements = hint.requirements.get("prerequisites", []) + # We allow public accessing of hints if challenges are visible and there is no cost or prerequisites + # If there is a cost or a prereq we should block the user from seeing the hint + if user is None: + if hint.cost or hint.prerequisites: + return ( + { + "success": False, + "errors": {"cost": ["You must login to unlock this hint"]}, + }, + 403, + ) + + if hint.prerequisites: + requirements = hint.prerequisites # Get the IDs of all hints that the user has unlocked all_unlocks = HintUnlocks.query.filter_by(account_id=user.account_id).all() diff --git a/CTFd/models/__init__.py b/CTFd/models/__init__.py index 713bfc3f..45f9d21b 100644 --- a/CTFd/models/__init__.py +++ b/CTFd/models/__init__.py @@ -181,6 +181,12 @@ class Hints(db.Model): return markup(build_markdown(self.content)) + @property + def prerequisites(self): + if self.requirements: + return self.requirements.get("prerequisites", []) + return [] + def __init__(self, *args, **kwargs): super(Hints, self).__init__(**kwargs) diff --git a/tests/api/v1/user/test_hints.py b/tests/api/v1/user/test_hints.py index ad69882c..e7609135 100644 --- a/tests/api/v1/user/test_hints.py +++ b/tests/api/v1/user/test_hints.py @@ -3,6 +3,7 @@ from freezegun import freeze_time +from CTFd.models import Hints from CTFd.utils import set_config from tests.helpers import ( create_ctfd, @@ -180,3 +181,90 @@ def test_api_hint_admin_access(): r_admin = admin.delete("/api/v1/hints/1", json="") assert r_admin.status_code == 200 destroy_ctfd(app) + + +def test_api_hints_accessible_public(): + """Test that hints with no cost and no prerequsites can be viewed publicy""" + app = create_ctfd() + with app.app_context(): + # Set challenges to be visible publicly + set_config("challenge_visibility", "public") + + register_user(app) + chal = gen_challenge(app.db) + gen_hint( + app.db, chal.id, content="This is a free hint", cost=0, type="standard" + ) + gen_hint( + app.db, chal.id, content="This is a private hint", cost=1, type="standard" + ) + gen_hint( + app.db, chal.id, content="This is a private hint", cost=1, type="standard" + ) + hint = Hints.query.filter_by(id=3).first() + hint.requirements = {"prerequisites": [2]} + app.db.session.commit() + + with app.test_client() as non_logged_in_user: + r = non_logged_in_user.get("/api/v1/hints/1") + hint = r.get_json()["data"] + assert hint["content"] == "This is a free hint" + + r = non_logged_in_user.get("/api/v1/hints/2") + assert r.status_code == 403 + errors = r.get_json()["errors"] + assert errors == {"cost": ["You must login to unlock this hint"]} + + r = non_logged_in_user.get("/api/v1/hints/3") + assert r.status_code == 403 + errors = r.get_json()["errors"] + assert errors == {"cost": ["You must login to unlock this hint"]} + + r = non_logged_in_user.post( + "/api/v1/unlocks", json={"target": 2, "type": "hints"} + ) + assert r.status_code == 403 + + # Set challenges to be visible to only authed + set_config("challenge_visibility", "private") + + # Free hints no longer visible to unauthed + with app.test_client() as non_logged_in_user: + r = non_logged_in_user.get("/api/v1/hints/1", json="") + assert r.status_code == 403 + + # Verify existing hint behavior for authed users + with login_as_user(app) as client: + r = client.get("/api/v1/hints/1") + hint = r.get_json()["data"] + assert hint["content"] == "This is a free hint" + + r = client.get("/api/v1/hints/2") + assert r.status_code == 200 + assert "content" not in r.get_json()["data"] + + r = client.get("/api/v1/hints/3") + assert r.status_code == 403 + + gen_award(app.db, 2) + + # Haven't unlocked the prereq hint + r = client.get("/api/v1/hints/3") + assert r.status_code == 403 + + # Unlock the prereq + r = client.post("/api/v1/unlocks", json={"target": 2, "type": "hints"}) + assert r.status_code == 200 + r = client.get("/api/v1/hints/2") + assert r.status_code == 200 + + # Attempt to unlock again but dont have the points + r = client.get("/api/v1/hints/3") + assert r.status_code == 200 + assert "content" not in r.get_json()["data"] + + r = client.post("/api/v1/unlocks", json={"target": 3, "type": "hints"}) + assert r.status_code == 200 + r = client.get("/api/v1/hints/3") + assert r.status_code == 200 + assert "content" in r.get_json()["data"]