diff --git a/.dockerignore b/.dockerignore index 5ef7a65b4..7937a735f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,3 +15,5 @@ CTFd/uploads/**/* **/node_modules **/*.pyc **/__pycache__ +.venv* +venv* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6d3387012..026123068 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ __pycache__/ # Distribution / packaging .Python env/ +venv* +.venv* build/ develop-eggs/ dist/ diff --git a/.prettierignore b/.prettierignore index ba40c14ae..00d79fc65 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,6 @@ CTFd/themes/**/vendor/ *.svg *.mp3 *.webm +.pytest_cache +venv* +.venv* diff --git a/CTFd/auth.py b/CTFd/auth.py index c6454cdaa..cb1baa376 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -1,7 +1,7 @@ import base64 import requests -from flask import Blueprint +from flask import Blueprint, abort from flask import current_app as app from flask import redirect, render_template, request, session, url_for from itsdangerous.exc import BadSignature, BadTimeSignature, SignatureExpired @@ -510,6 +510,16 @@ def oauth_redirect(): team = Teams.query.filter_by(oauth_id=team_id).first() if team is None: + num_teams_limit = int(get_config("num_teams", default=0)) + num_teams = Teams.query.filter_by( + banned=False, hidden=False + ).count() + if num_teams_limit and num_teams >= num_teams_limit: + abort( + 403, + description=f"Reached the maximum number of teams ({num_teams_limit}). Please join an existing team.", + ) + team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id) db.session.add(team) db.session.commit() diff --git a/CTFd/forms/config.py b/CTFd/forms/config.py index 51ba3d563..618322fc1 100644 --- a/CTFd/forms/config.py +++ b/CTFd/forms/config.py @@ -43,6 +43,9 @@ class AccountSettingsForm(BaseForm): widget=NumberInput(min=0), description="Amount of users per team (Teams mode only)", ) + num_teams = IntegerField( + widget=NumberInput(min=0), description="Max number of teams (Teams mode only)", + ) verify_emails = SelectField( "Verify Emails", description="Control whether users must confirm their email addresses before playing", diff --git a/CTFd/teams.py b/CTFd/teams.py index 1b99d6afb..945974451 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -197,6 +197,14 @@ def new(): description="Team creation is currently disabled. Please join an existing team.", ) + num_teams_limit = int(get_config("num_teams", default=0)) + num_teams = Teams.query.filter_by(banned=False, hidden=False).count() + if num_teams_limit and num_teams >= num_teams_limit: + abort( + 403, + description=f"Reached the maximum number of teams ({num_teams_limit}). Please join an existing team.", + ) + user = get_current_user_attrs() if user.team_id: errors.append("You are already in a team. You cannot join another.") diff --git a/CTFd/themes/admin/templates/configs/accounts.html b/CTFd/themes/admin/templates/configs/accounts.html index e870ec2bb..4f855220a 100644 --- a/CTFd/themes/admin/templates/configs/accounts.html +++ b/CTFd/themes/admin/templates/configs/accounts.html @@ -38,6 +38,14 @@ +
+ {{ form.num_teams.label }} + {{ form.num_teams(class="form-control", value=num_teams) }} + + {{ form.num_teams.description }} + +
+
{{ form.team_disbanding.label }} {{ form.team_disbanding(class="form-control", value=team_disbanding) }} diff --git a/tests/oauth/test_teams.py b/tests/oauth/test_teams.py index d52ba97ba..7b60919b3 100644 --- a/tests/oauth/test_teams.py +++ b/tests/oauth/test_teams.py @@ -30,3 +30,42 @@ def test_team_size_limit(): login_with_mlc(app, team_name="team_name", team_oauth_id=1234) assert len(Teams.query.filter_by(id=team_id).first().members) == 2 destroy_ctfd(app) + + +def test_num_teams_limit(): + """Only num_teams teams can be created even via MLC""" + app = create_ctfd(user_mode="teams") + app.config.update( + { + "OAUTH_CLIENT_ID": "ctfd_testing_client_id", + "OAUTH_CLIENT_SECRET": "ctfd_testing_client_secret", + "OAUTH_AUTHORIZATION_ENDPOINT": "http://auth.localhost/oauth/authorize", + "OAUTH_TOKEN_ENDPOINT": "http://auth.localhost/oauth/token", + "OAUTH_API_ENDPOINT": "http://api.localhost/user", + } + ) + with app.app_context(): + set_config("num_teams", 1) + gen_team(app.db, member_count=1, oauth_id=1234) + login_with_mlc( + app, + name="foobar", + email="foobar@a.com", + oauth_id=111, + team_name="foobar", + team_oauth_id=1111, + raise_for_error=False, + ) + assert Teams.query.count() == 1 + + set_config("num_teams", 2) + login_with_mlc( + app, + name="foobarbaz", + email="foobarbaz@a.com", + oauth_id=222, + team_name="foobarbaz", + team_oauth_id=2222, + ) + assert Teams.query.count() == 2 + destroy_ctfd(app) diff --git a/tests/teams/test_teams.py b/tests/teams/test_teams.py index 810240e7d..5ab31c35c 100644 --- a/tests/teams/test_teams.py +++ b/tests/teams/test_teams.py @@ -181,6 +181,40 @@ def test_team_size_limit(): destroy_ctfd(app) +def test_num_teams_limit(): + """Only num_teams teams can be created""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + set_config("num_teams", 1) + + # Create a team + gen_team(app.db, member_count=1) + + register_user(app) + with login_as_user(app) as client: + r = client.get("/teams/new") + assert r.status_code == 403 + + # team should be blocked from creation + with client.session_transaction() as sess: + data = { + "name": "team1", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/teams/new", data=data) + resp = r.get_data(as_text=True) + assert Teams.query.count() == 1 + assert "Reached the maximum number of teams" in resp + + # Can the team be created after the num has been bumped + set_config("num_teams", 2) + r = client.post("/teams/new", data=data) + resp = r.get_data(as_text=True) + assert Teams.query.count() == 2 + destroy_ctfd(app) + + def test_team_creation_disable(): app = create_ctfd(user_mode="teams") with app.app_context():