From 5e8ff5d892f367f9460657f6233189404eb09c6f Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 13 Jun 2023 17:02:30 -0400 Subject: [PATCH 1/4] Rough implementation of user registration limit --- CTFd/auth.py | 17 +++++++++++++++++ CTFd/forms/config.py | 4 ++++ .../admin/templates/configs/accounts.html | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/CTFd/auth.py b/CTFd/auth.py index 739558ea..870799f0 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -211,6 +211,14 @@ def register(): valid_email = validators.validate_email(email_address) team_name_email_check = validators.validate_email(name) + num_users_limit = int(get_config("num_users", default=0)) + num_users = Users.query.filter_by(banned=False, hidden=False).count() + if num_users_limit and num_users >= num_users_limit: + abort( + 403, + description=f"Reached the maximum number of users ({num_users_limit}).", + ) + if get_config("registration_code"): if ( registration_code.lower() @@ -490,6 +498,15 @@ def oauth_redirect(): user = Users.query.filter_by(email=user_email).first() if user is None: + # Respect the user count limit + num_users_limit = int(get_config("num_users", default=0)) + num_users = Users.query.filter_by(banned=False, hidden=False).count() + if num_users_limit and num_users >= num_users_limit: + abort( + 403, + description=f"Reached the maximum number of users ({num_users_limit}).", + ) + # Check if we are allowing registration before creating users if registration_visible() or mlc_registration(): user = Users( diff --git a/CTFd/forms/config.py b/CTFd/forms/config.py index da6e00ec..4bdb3588 100644 --- a/CTFd/forms/config.py +++ b/CTFd/forms/config.py @@ -48,6 +48,10 @@ class AccountSettingsForm(BaseForm): widget=NumberInput(min=0), description="Max number of teams (Teams mode only)", ) + num_users = IntegerField( + widget=NumberInput(min=0), + description="Max number of users", + ) verify_emails = SelectField( "Verify Emails", description="Control whether users must confirm their email addresses before playing", diff --git a/CTFd/themes/admin/templates/configs/accounts.html b/CTFd/themes/admin/templates/configs/accounts.html index 756551a6..f67dd60d 100644 --- a/CTFd/themes/admin/templates/configs/accounts.html +++ b/CTFd/themes/admin/templates/configs/accounts.html @@ -46,6 +46,14 @@ +
+ {{ form.num_users.label }} + {{ form.num_users(class="form-control", value=num_users) }} + + {{ form.num_users.description }} + +
+
{{ form.team_disbanding.label }} {{ form.team_disbanding(class="form-control", value=team_disbanding) }} From 5d055f60f6b7208b5e495c6925270b47e9dad906 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 13 Jun 2023 20:02:15 -0400 Subject: [PATCH 2/4] Add test for user limit registration behavior --- CTFd/auth.py | 16 +++++++-------- tests/users/test_users.py | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/CTFd/auth.py b/CTFd/auth.py index 870799f0..aacea14b 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -189,6 +189,14 @@ def register(): if current_user.authed(): return redirect(url_for("challenges.listing")) + num_users_limit = int(get_config("num_users", default=0)) + num_users = Users.query.filter_by(banned=False, hidden=False).count() + if num_users_limit and num_users >= num_users_limit: + abort( + 403, + description=f"Reached the maximum number of users ({num_users_limit}).", + ) + if request.method == "POST": name = request.form.get("name", "").strip() email_address = request.form.get("email", "").strip().lower() @@ -211,14 +219,6 @@ def register(): valid_email = validators.validate_email(email_address) team_name_email_check = validators.validate_email(name) - num_users_limit = int(get_config("num_users", default=0)) - num_users = Users.query.filter_by(banned=False, hidden=False).count() - if num_users_limit and num_users >= num_users_limit: - abort( - 403, - description=f"Reached the maximum number of users ({num_users_limit}).", - ) - if get_config("registration_code"): if ( registration_code.lower() diff --git a/tests/users/test_users.py b/tests/users/test_users.py index 3419843b..22e3c883 100644 --- a/tests/users/test_users.py +++ b/tests/users/test_users.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from CTFd.models import Users +from CTFd.utils import set_config from tests.helpers import ( create_ctfd, destroy_ctfd, @@ -107,3 +108,44 @@ def test_hidden_user_visibility(): response = r.get_data(as_text=True) assert user_name in response destroy_ctfd(app) + + +def test_num_users_limit(): + """Only num_users users can be created""" + app = create_ctfd() + with app.app_context(): + set_config("num_users", 1) + + register_user(app) + with app.test_client() as client: + r = client.get("/register") + assert r.status_code == 403 + + # team should be blocked from creation + with client.session_transaction() as sess: + data = { + "name": "user", + "email": "user@examplectf.com", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/register", data=data) + resp = r.get_data(as_text=True) + # This number is 2 to account for the admin and the registered user + assert Users.query.count() == 2 + assert "Reached the maximum number of users" in resp + + # Can the team be created after the num has been bumped + set_config("num_users", 2) + with client.session_transaction() as sess: + data = { + "name": "user1", + "email": "user1@examplectf.com", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/register", data=data) + resp = r.get_data(as_text=True) + assert r.status_code == 302 + assert Users.query.count() == 3 + destroy_ctfd(app) From d5c40142fb3b0f7a6c91c1bc57631d0f096d0ea0 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 13 Jun 2023 20:36:32 -0400 Subject: [PATCH 3/4] Fix lint --- CTFd/forms/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CTFd/forms/config.py b/CTFd/forms/config.py index 4bdb3588..bc68dee6 100644 --- a/CTFd/forms/config.py +++ b/CTFd/forms/config.py @@ -49,8 +49,7 @@ class AccountSettingsForm(BaseForm): description="Max number of teams (Teams mode only)", ) num_users = IntegerField( - widget=NumberInput(min=0), - description="Max number of users", + widget=NumberInput(min=0), description="Max number of users", ) verify_emails = SelectField( "Verify Emails", From e4b91dfe585c99a8b6849e46def884f833a2ab86 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 13 Jun 2023 20:53:07 -0400 Subject: [PATCH 4/4] Add test for num_user limit via MLC --- tests/oauth/test_users.py | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/oauth/test_users.py diff --git a/tests/oauth/test_users.py b/tests/oauth/test_users.py new file mode 100644 index 00000000..a53ecccd --- /dev/null +++ b/tests/oauth/test_users.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from CTFd.models import Users +from CTFd.utils import set_config +from tests.helpers import create_ctfd, destroy_ctfd, login_with_mlc, register_user + + +def test_num_users_oauth_limit(): + """Only num_users users can be created even via MLC""" + app = create_ctfd() + 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(): + register_user(app) + # There should be the admin and our registered user + assert Users.query.count() == 2 + set_config("num_users", 1) + + # This registration should fail and we should still have 2 users + login_with_mlc( + app, + name="foobarbaz", + email="foobarbaz@a.com", + oauth_id=111, + scope="profile", + raise_for_error=False, + ) + assert Users.query.count() == 2 + + # We increment num_users to 2 and then login again + set_config("num_users", 2) + login_with_mlc( + app, + name="foobarbaz", + email="foobarbaz@a.com", + oauth_id=111, + scope="profile", + ) + # The above login should have succeeded + assert Users.query.count() == 3 + destroy_ctfd(app)