mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 00:53:08 +00:00 
			
		
		
		
	Introduce honeypot for login/registering/password changing
This commit is contained in:
		| @@ -33,6 +33,7 @@ | ||||
|         {% endif %} | ||||
|  | ||||
|         {% csrf_token %} | ||||
|         {% render_honeypot_field %} | ||||
|  | ||||
|         <div> | ||||
|             <label for="{{ form.username.name }}">{{ form.username.label }}</label> | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| {% block content %} | ||||
| <form method="post" action=""> | ||||
| {% csrf_token %} | ||||
| {% render_honeypot_field %} | ||||
| {{ form.as_p() }} | ||||
| <input type="submit" value="{% trans %}Reset{% endtrans %}" /> | ||||
| </form> | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
|     {% else %} | ||||
|         <form action="{{ url('core:register') }}" method="post"> | ||||
|             {% csrf_token %} | ||||
|             {% render_honeypot_field %} | ||||
|             {{ form }} | ||||
|             <input type="submit" value="{% trans %}Register{% endtrans %}" /> | ||||
|         </form> | ||||
|   | ||||
							
								
								
									
										58
									
								
								core/templatetags/extensions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								core/templatetags/extensions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| # | ||||
| # Copyright 2024 | ||||
| # - Sli <antoine@bartuccio.fr> | ||||
| # | ||||
| # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, | ||||
| # http://ae.utbm.fr. | ||||
| # | ||||
| # This program is free software; you can redistribute it and/or modify it under | ||||
| # the terms of the GNU General Public License a published by the Free Software | ||||
| # Foundation; either version 3 of the License, or (at your option) any later | ||||
| # version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, but WITHOUT | ||||
| # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||
| # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more | ||||
| # details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License along with | ||||
| # this program; if not, write to the Free Software Foundation, Inc., 59 Temple | ||||
| # Place - Suite 330, Boston, MA 02111-1307, USA. | ||||
| # | ||||
| # | ||||
| from typing import Callable | ||||
|  | ||||
| import honeypot.templatetags.honeypot as honeypot_filters | ||||
| from django.template.loader import render_to_string | ||||
| from jinja2 import Environment, nodes | ||||
| from jinja2.ext import Extension | ||||
| from jinja2.parser import Parser | ||||
|  | ||||
|  | ||||
| class HoneypotExtension(Extension): | ||||
|     """ | ||||
|     Wrapper around the honeypot extension tag | ||||
|     Known limitation: doesn't support arguments | ||||
|  | ||||
|     Usage: {% render_honeypot_field %} | ||||
|     """ | ||||
|  | ||||
|     tags = {"render_honeypot_field"} | ||||
|  | ||||
|     def __init__(self, environment: Environment) -> None: | ||||
|         environment.globals["render_honeypot_field"] = ( | ||||
|             honeypot_filters.render_honeypot_field | ||||
|         ) | ||||
|         self.environment = environment | ||||
|  | ||||
|     def parse(self, parser: Parser) -> nodes.Output: | ||||
|         lineno = parser.stream.expect("name:render_honeypot_field").lineno | ||||
|         call = self.call_method( | ||||
|             "_render", | ||||
|             [nodes.Name("render_honeypot_field", "load", lineno=lineno)], | ||||
|             lineno=lineno, | ||||
|         ) | ||||
|         return nodes.Output([nodes.MarkSafe(call)]) | ||||
|  | ||||
|     def _render(self, render_honeypot_field: Callable[[str | None], str]): | ||||
|         return render_to_string("honeypot/honeypot_field.html", render_honeypot_field()) | ||||
| @@ -36,6 +36,7 @@ class TestUserRegistration: | ||||
|     @pytest.fixture() | ||||
|     def valid_payload(self): | ||||
|         return { | ||||
|             settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, | ||||
|             "first_name": "this user does not exist (yet)", | ||||
|             "last_name": "this user does not exist (yet)", | ||||
|             "email": "i-dont-exist-yet@git.an", | ||||
| @@ -68,6 +69,13 @@ class TestUserRegistration: | ||||
|         assert response.status_code == 200 | ||||
|         assert "TEST_REGISTER_USER_FORM_FAIL" in str(response.content) | ||||
|  | ||||
|     def test_register_honeypot_fail(self, client, valid_payload): | ||||
|         payload = valid_payload | { | ||||
|             settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE + "random" | ||||
|         } | ||||
|         response = client.post(reverse("core:register"), payload) | ||||
|         assert response.status_code == 400 | ||||
|  | ||||
|     def test_register_user_form_fail_already_exists(self, client, valid_payload): | ||||
|         """Should not register a user correctly if it already exists.""" | ||||
|         # create the user, then try to create it again | ||||
| @@ -90,7 +98,11 @@ class TestUserLogin: | ||||
|  | ||||
|         response = client.post( | ||||
|             reverse("core:login"), | ||||
|             {"username": user.username, "password": "wrong-password"}, | ||||
|             { | ||||
|                 "username": user.username, | ||||
|                 "password": "wrong-password", | ||||
|                 settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, | ||||
|             }, | ||||
|         ) | ||||
|         assert response.status_code == 200 | ||||
|         assert ( | ||||
| @@ -98,12 +110,28 @@ class TestUserLogin: | ||||
|             "et votre mot de passe ne correspondent pas. Merci de réessayer.</p>" | ||||
|         ) in str(response.content.decode()) | ||||
|  | ||||
|     def test_login_honeypot(self, client, user): | ||||
|         response = client.post( | ||||
|             reverse("core:login"), | ||||
|             { | ||||
|                 "username": user.username, | ||||
|                 "password": "wrong-password", | ||||
|                 settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE + "incorrect", | ||||
|             }, | ||||
|         ) | ||||
|         assert response.status_code == 400 | ||||
|  | ||||
|     def test_login_success(self, client, user): | ||||
|         """ | ||||
|         Should login a user correctly | ||||
|         """ | ||||
|         response = client.post( | ||||
|             reverse("core:login"), {"username": user.username, "password": "plop"} | ||||
|             reverse("core:login"), | ||||
|             { | ||||
|                 "username": user.username, | ||||
|                 "password": "plop", | ||||
|                 settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, | ||||
|             }, | ||||
|         ) | ||||
|         assertRedirects(response, reverse("core:index")) | ||||
|  | ||||
|   | ||||
| @@ -36,6 +36,7 @@ from django.http import Http404, HttpResponse | ||||
| from django.shortcuts import get_object_or_404, redirect, render | ||||
| from django.template.response import TemplateResponse | ||||
| from django.urls import reverse, reverse_lazy | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ( | ||||
|     CreateView, | ||||
| @@ -46,6 +47,7 @@ from django.views.generic import ( | ||||
| ) | ||||
| from django.views.generic.dates import MonthMixin, YearMixin | ||||
| from django.views.generic.edit import UpdateView | ||||
| from honeypot.decorators import check_honeypot | ||||
|  | ||||
| from api.views.sas import all_pictures_of_user | ||||
| from core.models import Gift, Preferences, SithFile, User | ||||
| @@ -69,6 +71,7 @@ from subscription.models import Subscription | ||||
| from trombi.views import UserTrombiForm | ||||
|  | ||||
|  | ||||
| @method_decorator(check_honeypot, name="post") | ||||
| class SithLoginView(views.LoginView): | ||||
|     """ | ||||
|     The login View | ||||
| @@ -124,9 +127,10 @@ def password_root_change(request, user_id): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @method_decorator(check_honeypot, name="post") | ||||
| class SithPasswordResetView(views.PasswordResetView): | ||||
|     """ | ||||
|     Allows someone to enter an email adresse for resetting password | ||||
|     Allows someone to enter an email address for resetting password | ||||
|     """ | ||||
|  | ||||
|     template_name = "core/password_reset.jinja" | ||||
| @@ -153,12 +157,13 @@ class SithPasswordResetConfirmView(views.PasswordResetConfirmView): | ||||
|  | ||||
| class SithPasswordResetCompleteView(views.PasswordResetCompleteView): | ||||
|     """ | ||||
|     Confirm the password has sucessfully been reset | ||||
|     Confirm the password has successfully been reset | ||||
|     """ | ||||
|  | ||||
|     template_name = "core/password_reset_complete.jinja" | ||||
|  | ||||
|  | ||||
| @check_honeypot | ||||
| def register(request): | ||||
|     context = {} | ||||
|     if request.method == "POST": | ||||
|   | ||||
							
								
								
									
										36
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										36
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @@ -426,13 +426,13 @@ files = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "django" | ||||
| version = "4.2.13" | ||||
| version = "4.2.14" | ||||
| description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, | ||||
|     {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, | ||||
|     {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, | ||||
|     {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @@ -477,17 +477,17 @@ test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytes | ||||
|  | ||||
| [[package]] | ||||
| name = "django-debug-toolbar" | ||||
| version = "4.3.0" | ||||
| version = "4.4.5" | ||||
| description = "A configurable set of panels that display various debug information about the current request/response." | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"}, | ||||
|     {file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"}, | ||||
|     {file = "django_debug_toolbar-4.4.5-py3-none-any.whl", hash = "sha256:91425606673ee674d780f7aeedf3595c264eb382dcf41f55c6779577900904c0"}, | ||||
|     {file = "django_debug_toolbar-4.4.5.tar.gz", hash = "sha256:8298ce966b4c8fc71430082dd4739ef2badb5f867734e1973a413c4ab2ea81b7"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| django = ">=3.2.4" | ||||
| django = ">=4.2.9" | ||||
| sqlparse = ">=0.2" | ||||
|  | ||||
| [[package]] | ||||
| @@ -508,6 +508,20 @@ packaging = "*" | ||||
| elasticsearch = ["elasticsearch (>=5,<8)"] | ||||
| testing = ["coverage", "geopy (==2)", "pysolr (>=3.7)", "python-dateutil", "requests", "whoosh (>=2.5.4,<3.0)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "django-honeypot" | ||||
| version = "1.2.0" | ||||
| description = "Django honeypot field utilities" | ||||
| optional = false | ||||
| python-versions = "<4.0,>=3.8" | ||||
| files = [ | ||||
|     {file = "django_honeypot-1.2.0-py3-none-any.whl", hash = "sha256:53dd5f8dd96ef1bb7e31b5514c0dc2caae9577e78ebdf03ca4e0f304a7422aba"}, | ||||
|     {file = "django_honeypot-1.2.0.tar.gz", hash = "sha256:25fca02e786aec26649bd13b37a95c846e09ab3cfc10f28db2f7dfaa77b9b9c6"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| Django = ">=3.2,<5.1" | ||||
|  | ||||
| [[package]] | ||||
| name = "django-jinja" | ||||
| version = "2.11.0" | ||||
| @@ -669,13 +683,13 @@ python-dateutil = ">=2.7" | ||||
|  | ||||
| [[package]] | ||||
| name = "identify" | ||||
| version = "2.5.36" | ||||
| version = "2.6.0" | ||||
| description = "File identification library for Python" | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, | ||||
|     {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, | ||||
|     {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, | ||||
|     {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, | ||||
| ] | ||||
|  | ||||
| [package.extras] | ||||
| @@ -1867,4 +1881,4 @@ filelock = ">=3.4" | ||||
| [metadata] | ||||
| lock-version = "2.0" | ||||
| python-versions = "^3.10" | ||||
| content-hash = "8524ed5f593973edf05b3c01010c1f2345b7e799089c3e38274304bdedf8b3cb" | ||||
| content-hash = "51820883f41bdf40f00296b722ebdd9ac386e43ef1424ef990b29bac579ecbab" | ||||
|   | ||||
| @@ -46,6 +46,7 @@ django-countries = "^7.5.1" | ||||
| dict2xml = "^1.7.3" | ||||
| Sphinx = "^5" # Needed for building xapian | ||||
| tomli = "^2.0.1" | ||||
| django-honeypot = "^1.2.0" | ||||
|  | ||||
| [tool.poetry.group.dev.dependencies] | ||||
| django-debug-toolbar = "^4.0.0" | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # | ||||
| # Copyright 2016,2017 | ||||
| # Copyright 2016,2017,2024 | ||||
| # - Skia <skia@libskia.so> | ||||
| # - Sli <antoine@bartuccio.fr> | ||||
| # | ||||
| @@ -17,7 +17,7 @@ | ||||
| # details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License along with | ||||
| # this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple | ||||
| # this program; if not, write to the Free Software Foundation, Inc., 59 Temple | ||||
| # Place - Suite 330, Boston, MA 02111-1307, USA. | ||||
| # | ||||
| # | ||||
| @@ -54,6 +54,10 @@ os.environ["HTTPS"] = "off" | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = "(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2" | ||||
|  | ||||
| # Those values are to be changed in production to be more effective | ||||
| HONEYPOT_FIELD_NAME = "body2" | ||||
| HONEYPOT_VALUE = "content" | ||||
|  | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = False | ||||
| TESTING = "pytest" in sys.modules | ||||
| @@ -75,6 +79,7 @@ INSTALLED_APPS = ( | ||||
|     "django.contrib.messages", | ||||
|     "django.contrib.staticfiles", | ||||
|     "django.contrib.sites", | ||||
|     "honeypot", | ||||
|     "django_jinja", | ||||
|     "rest_framework", | ||||
|     "ajax_select", | ||||
| @@ -143,6 +148,7 @@ TEMPLATES = [ | ||||
|                 "django_jinja.builtins.extensions.UrlsExtension", | ||||
|                 "django_jinja.builtins.extensions.StaticFilesExtension", | ||||
|                 "django_jinja.builtins.extensions.DjangoFiltersExtension", | ||||
|                 "core.templatetags.extensions.HoneypotExtension", | ||||
|             ], | ||||
|             "filters": { | ||||
|                 "markdown": "core.templatetags.renderer.markdown", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user