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.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("/<hint_id>")
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()

View File

@@ -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)

View File

@@ -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"]