Merge pull request #2333 from CTFd/2157-free-hints-view-public

* Free hints (those without a cost or prerequsitites) can now be viewed publicly if challenges are visible publicly
* Closes #2157
This commit is contained in:
Kevin Chung
2023-06-16 16:51:14 -04:00
committed by GitHub
3 changed files with 112 additions and 5 deletions

View File

@@ -9,7 +9,8 @@ from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessRespon
from CTFd.constants import RawEnum from CTFd.constants import RawEnum
from CTFd.models import Hints, HintUnlocks, db from CTFd.models import Hints, HintUnlocks, db
from CTFd.schemas.hints import HintSchema 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.helpers.models import build_model_filters
from CTFd.utils.user import get_current_user, is_admin from CTFd.utils.user import get_current_user, is_admin
@@ -105,7 +106,7 @@ class HintList(Resource):
@hints_namespace.route("/<hint_id>") @hints_namespace.route("/<hint_id>")
class Hint(Resource): class Hint(Resource):
@during_ctf_time_only @during_ctf_time_only
@authed_only @check_challenge_visibility
@hints_namespace.doc( @hints_namespace.doc(
description="Endpoint to get a specific Hint object", description="Endpoint to get a specific Hint object",
responses={ responses={
@@ -117,11 +118,23 @@ class Hint(Resource):
}, },
) )
def get(self, hint_id): def get(self, hint_id):
user = get_current_user()
hint = Hints.query.filter_by(id=hint_id).first_or_404() hint = Hints.query.filter_by(id=hint_id).first_or_404()
user = get_current_user()
if hint.requirements: # We allow public accessing of hints if challenges are visible and there is no cost or prerequisites
requirements = hint.requirements.get("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 # Get the IDs of all hints that the user has unlocked
all_unlocks = HintUnlocks.query.filter_by(account_id=user.account_id).all() all_unlocks = HintUnlocks.query.filter_by(account_id=user.account_id).all()

View File

@@ -181,6 +181,12 @@ class Hints(db.Model):
return markup(build_markdown(self.content)) return markup(build_markdown(self.content))
@property
def prerequisites(self):
if self.requirements:
return self.requirements.get("prerequisites", [])
return []
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Hints, self).__init__(**kwargs) super(Hints, self).__init__(**kwargs)

View File

@@ -3,6 +3,7 @@
from freezegun import freeze_time from freezegun import freeze_time
from CTFd.models import Hints
from CTFd.utils import set_config from CTFd.utils import set_config
from tests.helpers import ( from tests.helpers import (
create_ctfd, create_ctfd,
@@ -180,3 +181,90 @@ def test_api_hint_admin_access():
r_admin = admin.delete("/api/v1/hints/1", json="") r_admin = admin.delete("/api/v1/hints/1", json="")
assert r_admin.status_code == 200 assert r_admin.status_code == 200
destroy_ctfd(app) 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"]