Repository: CTFd/CTFd

Issue:
Don't require teams for viewing challenges if challenges are public
Looks like if challenges are set to be public but teams are required then challenges can't be seen. This requirement needs to be relaxed a bit. 


Discussion:

Resolving patch:
diff --git a/CTFd/challenges.py b/CTFd/challenges.py
--- a/CTFd/challenges.py
+++ b/CTFd/challenges.py
@@ -1,14 +1,12 @@
-from flask import Blueprint, render_template
+from flask import Blueprint, redirect, render_template, request, url_for
 
-from CTFd.utils import config
+from CTFd.constants.config import ChallengeVisibilityTypes, Configs
+from CTFd.utils.config import is_teams_mode
 from CTFd.utils.dates import ctf_ended, ctf_paused, ctf_started
-from CTFd.utils.decorators import (
-    during_ctf_time_only,
-    require_team,
-    require_verified_emails,
-)
+from CTFd.utils.decorators import during_ctf_time_only, require_verified_emails
 from CTFd.utils.decorators.visibility import check_challenge_visibility
 from CTFd.utils.helpers import get_errors, get_infos
+from CTFd.utils.user import authed, get_current_team
 
 challenges = Blueprint("challenges", __name__)
 
@@ -17,18 +15,26 @@
 @during_ctf_time_only
 @require_verified_emails
 @check_challenge_visibility
-@require_team
 def listing():
+    if (
+        Configs.challenge_visibility == ChallengeVisibilityTypes.PUBLIC
+        and authed() is False
+    ):
+        pass
+    else:
+        if is_teams_mode() and get_current_team() is None:
+            return redirect(url_for("teams.private", next=request.full_path))
+
     infos = get_infos()
     errors = get_errors()
 
     if ctf_started() is False:
-        errors.append(f"{config.ctf_name()} has not started yet")
+        errors.append(f"{Configs.ctf_name} has not started yet")
 
     if ctf_paused() is True:
-        infos.append(f"{config.ctf_name()} is paused")
+        infos.append(f"{Configs.ctf_name} is paused")
 
     if ctf_ended() is True:
-        infos.append(f"{config.ctf_name()} has ended")
+        infos.append(f"{Configs.ctf_name} has ended")
 
     return render_template("challenges.html", infos=infos, errors=errors)


Test patch:
diff --git a/tests/teams/test_challenges.py b/tests/teams/test_challenges.py
--- a/tests/teams/test_challenges.py
+++ b/tests/teams/test_challenges.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+from CTFd.utils import set_config
 from CTFd.utils.scores import get_standings
 from tests.helpers import (
     create_ctfd,
@@ -10,6 +11,7 @@
     gen_team,
     gen_user,
     login_as_user,
+    register_user,
 )
 
 
@@ -38,3 +40,26 @@ def test_challenge_team_submit():
         assert standings[0][2] == "team_name"
         assert standings[0][3] == 100
     destroy_ctfd(app)
+
+
+def test_anonymous_users_view_public_challenges_without_team():
+    """Test that if challenges are public, users without team can still view them"""
+    app = create_ctfd(user_mode="teams")
+    with app.app_context():
+        register_user(app)
+        gen_challenge(app.db)
+        with app.test_client() as client:
+            r = client.get("/challenges")
+            assert r.status_code == 302
+            assert r.location.startswith("http://localhost/login")
+
+        set_config("challenge_visibility", "public")
+        with app.test_client() as client:
+            r = client.get("/challenges")
+            assert r.status_code == 200
+
+        with login_as_user(app) as client:
+            r = client.get("/challenges")
+            assert r.status_code == 302
+            assert r.location.startswith("http://localhost/team")
+    destroy_ctfd(app)