mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
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:
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user