Repository: CTFd/CTFd

Issue:
API modifications to list all rows regardless of any filters
The API needs a way to list everything for admins. Admins are still subject to filtering mechanisms like hiding/requirements. 

For example https://github.com/CTFd/CTFd/pull/1165 shows a solution that someone pulled up. I think a similar approach is possible. 


Discussion:

Resolving patch:
diff --git a/CTFd/api/v1/challenges.py b/CTFd/api/v1/challenges.py
--- a/CTFd/api/v1/challenges.py
+++ b/CTFd/api/v1/challenges.py
@@ -47,31 +47,36 @@ def get(self):
         # This can return None (unauth) if visibility is set to public
         user = get_current_user()
 
-        challenges = (
-            Challenges.query.filter(
-                and_(Challenges.state != "hidden", Challenges.state != "locked")
-            )
-            .order_by(Challenges.value)
-            .all()
-        )
-
-        if user:
-            solve_ids = (
-                Solves.query.with_entities(Solves.challenge_id)
-                .filter_by(account_id=user.account_id)
-                .order_by(Solves.challenge_id.asc())
+        # Admins can request to see everything
+        if is_admin() and request.args.get("view") == "admin":
+            challenges = Challenges.query.order_by(Challenges.value).all()
+            solve_ids = set([challenge.id for challenge in challenges])
+        else:
+            challenges = (
+                Challenges.query.filter(
+                    and_(Challenges.state != "hidden", Challenges.state != "locked")
+                )
+                .order_by(Challenges.value)
                 .all()
             )
-            solve_ids = set([value for value, in solve_ids])
 
-            # TODO: Convert this into a re-useable decorator
-            if is_admin():
-                pass
+            if user:
+                solve_ids = (
+                    Solves.query.with_entities(Solves.challenge_id)
+                    .filter_by(account_id=user.account_id)
+                    .order_by(Solves.challenge_id.asc())
+                    .all()
+                )
+                solve_ids = set([value for value, in solve_ids])
+
+                # TODO: Convert this into a re-useable decorator
+                if is_admin():
+                    pass
+                else:
+                    if config.is_teams_mode() and get_current_team() is None:
+                        abort(403)
             else:
-                if config.is_teams_mode() and get_current_team() is None:
-                    abort(403)
-        else:
-            solve_ids = set()
+                solve_ids = set()
 
         response = []
         tag_schema = TagSchema(view="user", many=True)
diff --git a/CTFd/api/v1/teams.py b/CTFd/api/v1/teams.py
--- a/CTFd/api/v1/teams.py
+++ b/CTFd/api/v1/teams.py
@@ -22,7 +22,11 @@
 class TeamList(Resource):
     @check_account_visibility
     def get(self):
-        teams = Teams.query.filter_by(hidden=False, banned=False)
+        if is_admin() and request.args.get("view") == "admin":
+            teams = Teams.query.filter_by()
+        else:
+            teams = Teams.query.filter_by(hidden=False, banned=False)
+
         user_type = get_current_user_type(fallback="user")
         view = copy.deepcopy(TeamSchema.views.get(user_type))
         view.remove("members")
diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py
--- a/CTFd/api/v1/users.py
+++ b/CTFd/api/v1/users.py
@@ -31,7 +31,11 @@
 class UserList(Resource):
     @check_account_visibility
     def get(self):
-        users = Users.query.filter_by(banned=False, hidden=False)
+        if is_admin() and request.args.get("view") == "admin":
+            users = Users.query.filter_by()
+        else:
+            users = Users.query.filter_by(banned=False, hidden=False)
+
         response = UserSchema(view="user", many=True).dump(users)
 
         if response.errors:


Test patch:
diff --git a/tests/api/v1/test_challenges.py b/tests/api/v1/test_challenges.py
--- a/tests/api/v1/test_challenges.py
+++ b/tests/api/v1/test_challenges.py
@@ -135,6 +135,25 @@ def test_api_challenges_get_admin():
     destroy_ctfd(app)
 
 
+def test_api_challenges_get_hidden_admin():
+    """Can an admin see hidden challenges in API list response"""
+    app = create_ctfd()
+    with app.app_context():
+        gen_challenge(app.db, state="hidden")
+        gen_challenge(app.db)
+
+        with login_as_user(app, "admin") as admin:
+            challenges_list = admin.get("/api/v1/challenges", json="").get_json()[
+                "data"
+            ]
+            assert len(challenges_list) == 1
+            challenges_list = admin.get(
+                "/api/v1/challenges?view=admin", json=""
+            ).get_json()["data"]
+            assert len(challenges_list) == 2
+    destroy_ctfd(app)
+
+
 def test_api_challenges_post_admin():
     """Can a user post /api/v1/challenges if admin"""
     app = create_ctfd()
diff --git a/tests/api/v1/test_teams.py b/tests/api/v1/test_teams.py
--- a/tests/api/v1/test_teams.py
+++ b/tests/api/v1/test_teams.py
@@ -696,6 +696,9 @@ def test_api_accessing_hidden_banned_users():
         app.db.session.commit()
 
         with login_as_user(app, name="visible_user") as client:
+            list_teams = client.get("/api/v1/teams").get_json()["data"]
+            assert len(list_teams) == 0
+
             assert client.get("/api/v1/teams/1").status_code == 404
             assert client.get("/api/v1/teams/1/solves").status_code == 404
             assert client.get("/api/v1/teams/1/fails").status_code == 404
@@ -707,6 +710,10 @@ def test_api_accessing_hidden_banned_users():
             assert client.get("/api/v1/teams/2/awards").status_code == 404
 
         with login_as_user(app, name="admin") as client:
+            # Admins see hidden teams in lists
+            list_users = client.get("/api/v1/teams?view=admin").get_json()["data"]
+            assert len(list_users) == 2
+
             assert client.get("/api/v1/teams/1").status_code == 200
             assert client.get("/api/v1/teams/1/solves").status_code == 200
             assert client.get("/api/v1/teams/1/fails").status_code == 200
diff --git a/tests/api/v1/test_users.py b/tests/api/v1/test_users.py
--- a/tests/api/v1/test_users.py
+++ b/tests/api/v1/test_users.py
@@ -764,12 +764,19 @@ def test_api_accessing_hidden_users():
         app.db.session.commit()
 
         with login_as_user(app, name="visible_user") as client:
+            list_users = client.get("/api/v1/users").get_json()["data"]
+            assert len(list_users) == 1
+
             assert client.get("/api/v1/users/3").status_code == 404
             assert client.get("/api/v1/users/3/solves").status_code == 404
             assert client.get("/api/v1/users/3/fails").status_code == 404
             assert client.get("/api/v1/users/3/awards").status_code == 404
 
         with login_as_user(app, name="admin") as client:
+            # Admins see the user in lists
+            list_users = client.get("/api/v1/users?view=admin").get_json()["data"]
+            assert len(list_users) == 3
+
             assert client.get("/api/v1/users/3").status_code == 200
             assert client.get("/api/v1/users/3/solves").status_code == 200
             assert client.get("/api/v1/users/3/fails").status_code == 200
@@ -788,12 +795,19 @@ def test_api_accessing_banned_users():
         app.db.session.commit()
 
         with login_as_user(app, name="visible_user") as client:
+            list_users = client.get("/api/v1/users").get_json()["data"]
+            assert len(list_users) == 1
+
             assert client.get("/api/v1/users/3").status_code == 404
             assert client.get("/api/v1/users/3/solves").status_code == 404
             assert client.get("/api/v1/users/3/fails").status_code == 404
             assert client.get("/api/v1/users/3/awards").status_code == 404
 
         with login_as_user(app, name="admin") as client:
+            # Admins see the user in lists
+            list_users = client.get("/api/v1/users?view=admin").get_json()["data"]
+            assert len(list_users) == 3
+
             assert client.get("/api/v1/users/3").status_code == 200
             assert client.get("/api/v1/users/3/solves").status_code == 200
             assert client.get("/api/v1/users/3/fails").status_code == 200