diff --git a/CTFd/auth.py b/CTFd/auth.py index cb1baa37..0012098a 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -197,6 +197,7 @@ def register(): website = request.form.get("website") affiliation = request.form.get("affiliation") country = request.form.get("country") + registration_code = request.form.get("registration_code", "") name_len = len(name) == 0 names = Users.query.add_columns("name", "id").filter_by(name=name).first() @@ -210,6 +211,13 @@ def register(): valid_email = validators.validate_email(email_address) team_name_email_check = validators.validate_email(name) + if get_config("registration_code"): + if ( + registration_code.lower() + != get_config("registration_code", default="").lower() + ): + errors.append("The registration code you entered was incorrect") + # Process additional user fields fields = {} for field in UserFields.query.all(): diff --git a/CTFd/forms/auth.py b/CTFd/forms/auth.py index 41a653d9..a8c73c4f 100644 --- a/CTFd/forms/auth.py +++ b/CTFd/forms/auth.py @@ -4,7 +4,12 @@ from wtforms.validators import InputRequired from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField -from CTFd.forms.users import attach_custom_user_fields, build_custom_user_fields +from CTFd.forms.users import ( + attach_custom_user_fields, + attach_registration_code_field, + build_custom_user_fields, + build_registration_code_field, +) def RegistrationForm(*args, **kwargs): @@ -18,9 +23,10 @@ def RegistrationForm(*args, **kwargs): def extra(self): return build_custom_user_fields( self, include_entries=False, blacklisted_items=() - ) + ) + build_registration_code_field(self) attach_custom_user_fields(_RegistrationForm) + attach_registration_code_field(_RegistrationForm) return _RegistrationForm(*args, **kwargs) diff --git a/CTFd/forms/users.py b/CTFd/forms/users.py index 4962aa22..d157b2da 100644 --- a/CTFd/forms/users.py +++ b/CTFd/forms/users.py @@ -2,6 +2,7 @@ from wtforms import BooleanField, PasswordField, SelectField, StringField from wtforms.fields.html5 import EmailField from wtforms.validators import InputRequired +from CTFd.constants.config import Configs from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField from CTFd.models import UserFieldEntries, UserFields @@ -79,6 +80,36 @@ def attach_custom_user_fields(form_cls, **kwargs): setattr(form_cls, f"fields[{field.id}]", input_field) +def build_registration_code_field(form_cls): + """ + Build the appropriate field so we can render it via the extra property. + Add field_type so Jinja knows how to render it. + """ + if Configs.registration_code: + field = getattr(form_cls, "registration_code") # noqa B009 + field.field_type = "text" + return [field] + else: + return [] + + +def attach_registration_code_field(form_cls): + """ + If we have a registration code required, we attach it to the form similar + to attach_custom_user_fields + """ + if Configs.registration_code: + setattr( # noqa B010 + form_cls, + "registration_code", + StringField( + "Registration Code", + description="Registration code required to create account", + validators=[InputRequired()], + ), + ) + + class UserSearchForm(BaseForm): field = SelectField( "Search Field", diff --git a/CTFd/themes/admin/templates/config.html b/CTFd/themes/admin/templates/config.html index e2f26ea3..4086510a 100644 --- a/CTFd/themes/admin/templates/config.html +++ b/CTFd/themes/admin/templates/config.html @@ -32,6 +32,9 @@ + @@ -73,6 +76,8 @@ {% include "admin/configs/settings.html" %} + {% include "admin/configs/security.html" %} + {% include "admin/configs/email.html" %} {% include "admin/configs/time.html" %} diff --git a/CTFd/themes/admin/templates/configs/security.html b/CTFd/themes/admin/templates/configs/security.html new file mode 100644 index 00000000..a05c88ff --- /dev/null +++ b/CTFd/themes/admin/templates/configs/security.html @@ -0,0 +1,14 @@ +
+
+
+ + +
+ + +
+
diff --git a/tests/users/test_auth.py b/tests/users/test_auth.py index d4a7c65f..3d36c537 100644 --- a/tests/users/test_auth.py +++ b/tests/users/test_auth.py @@ -423,3 +423,44 @@ def test_banned_user(): r = client.get(route) assert r.status_code == 403 destroy_ctfd(app) + + +def test_registration_code_required(): + """ + Test that registration code configuration properly blocks logins + with missing and incorrect registration codes + """ + app = create_ctfd() + with app.app_context(): + # Set a registration code + set_config("registration_code", "secret-sauce") + + with app.test_client() as client: + # Load CSRF nonce + r = client.get("/register") + resp = r.get_data(as_text=True) + assert "Registration Code" in resp + with client.session_transaction() as sess: + data = { + "name": "user", + "email": "user1@examplectf.com", + "password": "password", + "nonce": sess.get("nonce"), + } + # Attempt registration without password + r = client.post("/register", data=data) + resp = r.get_data(as_text=True) + assert "The registration code you entered was incorrect" in resp + + # Attempt registration with wrong password + data["registration_code"] = "wrong-sauce" + r = client.post("/register", data=data) + resp = r.get_data(as_text=True) + assert "The registration code you entered was incorrect" in resp + + # Attempt registration with right password + data["registration_code"] = "secret-sauce" + r = client.post("/register", data=data) + assert r.status_code == 302 + assert r.location.startswith("http://localhost/challenges") + destroy_ctfd(app)