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 %} |         {% endif %} | ||||||
|  |  | ||||||
|         {% csrf_token %} |         {% csrf_token %} | ||||||
|  |         {% render_honeypot_field %} | ||||||
|  |  | ||||||
|         <div> |         <div> | ||||||
|             <label for="{{ form.username.name }}">{{ form.username.label }}</label> |             <label for="{{ form.username.name }}">{{ form.username.label }}</label> | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| {% block content %} | {% block content %} | ||||||
| <form method="post" action=""> | <form method="post" action=""> | ||||||
| {% csrf_token %} | {% csrf_token %} | ||||||
|  | {% render_honeypot_field %} | ||||||
| {{ form.as_p() }} | {{ form.as_p() }} | ||||||
| <input type="submit" value="{% trans %}Reset{% endtrans %}" /> | <input type="submit" value="{% trans %}Reset{% endtrans %}" /> | ||||||
| </form> | </form> | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ | |||||||
|     {% else %} |     {% else %} | ||||||
|         <form action="{{ url('core:register') }}" method="post"> |         <form action="{{ url('core:register') }}" method="post"> | ||||||
|             {% csrf_token %} |             {% csrf_token %} | ||||||
|  |             {% render_honeypot_field %} | ||||||
|             {{ form }} |             {{ form }} | ||||||
|             <input type="submit" value="{% trans %}Register{% endtrans %}" /> |             <input type="submit" value="{% trans %}Register{% endtrans %}" /> | ||||||
|         </form> |         </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() |     @pytest.fixture() | ||||||
|     def valid_payload(self): |     def valid_payload(self): | ||||||
|         return { |         return { | ||||||
|  |             settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, | ||||||
|             "first_name": "this user does not exist (yet)", |             "first_name": "this user does not exist (yet)", | ||||||
|             "last_name": "this user does not exist (yet)", |             "last_name": "this user does not exist (yet)", | ||||||
|             "email": "i-dont-exist-yet@git.an", |             "email": "i-dont-exist-yet@git.an", | ||||||
| @@ -68,6 +69,13 @@ class TestUserRegistration: | |||||||
|         assert response.status_code == 200 |         assert response.status_code == 200 | ||||||
|         assert "TEST_REGISTER_USER_FORM_FAIL" in str(response.content) |         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): |     def test_register_user_form_fail_already_exists(self, client, valid_payload): | ||||||
|         """Should not register a user correctly if it already exists.""" |         """Should not register a user correctly if it already exists.""" | ||||||
|         # create the user, then try to create it again |         # create the user, then try to create it again | ||||||
| @@ -90,7 +98,11 @@ class TestUserLogin: | |||||||
|  |  | ||||||
|         response = client.post( |         response = client.post( | ||||||
|             reverse("core:login"), |             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 response.status_code == 200 | ||||||
|         assert ( |         assert ( | ||||||
| @@ -98,12 +110,28 @@ class TestUserLogin: | |||||||
|             "et votre mot de passe ne correspondent pas. Merci de réessayer.</p>" |             "et votre mot de passe ne correspondent pas. Merci de réessayer.</p>" | ||||||
|         ) in str(response.content.decode()) |         ) 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): |     def test_login_success(self, client, user): | ||||||
|         """ |         """ | ||||||
|         Should login a user correctly |         Should login a user correctly | ||||||
|         """ |         """ | ||||||
|         response = client.post( |         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")) |         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.shortcuts import get_object_or_404, redirect, render | ||||||
| from django.template.response import TemplateResponse | from django.template.response import TemplateResponse | ||||||
| from django.urls import reverse, reverse_lazy | from django.urls import reverse, reverse_lazy | ||||||
|  | from django.utils.decorators import method_decorator | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from django.views.generic import ( | from django.views.generic import ( | ||||||
|     CreateView, |     CreateView, | ||||||
| @@ -46,6 +47,7 @@ from django.views.generic import ( | |||||||
| ) | ) | ||||||
| from django.views.generic.dates import MonthMixin, YearMixin | from django.views.generic.dates import MonthMixin, YearMixin | ||||||
| from django.views.generic.edit import UpdateView | from django.views.generic.edit import UpdateView | ||||||
|  | from honeypot.decorators import check_honeypot | ||||||
|  |  | ||||||
| from api.views.sas import all_pictures_of_user | from api.views.sas import all_pictures_of_user | ||||||
| from core.models import Gift, Preferences, SithFile, User | from core.models import Gift, Preferences, SithFile, User | ||||||
| @@ -69,6 +71,7 @@ from subscription.models import Subscription | |||||||
| from trombi.views import UserTrombiForm | from trombi.views import UserTrombiForm | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @method_decorator(check_honeypot, name="post") | ||||||
| class SithLoginView(views.LoginView): | class SithLoginView(views.LoginView): | ||||||
|     """ |     """ | ||||||
|     The login View |     The login View | ||||||
| @@ -124,9 +127,10 @@ def password_root_change(request, user_id): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @method_decorator(check_honeypot, name="post") | ||||||
| class SithPasswordResetView(views.PasswordResetView): | 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" |     template_name = "core/password_reset.jinja" | ||||||
| @@ -153,12 +157,13 @@ class SithPasswordResetConfirmView(views.PasswordResetConfirmView): | |||||||
|  |  | ||||||
| class SithPasswordResetCompleteView(views.PasswordResetCompleteView): | 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" |     template_name = "core/password_reset_complete.jinja" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @check_honeypot | ||||||
| def register(request): | def register(request): | ||||||
|     context = {} |     context = {} | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										36
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @@ -426,13 +426,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django" | 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." | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, |     {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, | ||||||
|     {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, |     {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @@ -477,17 +477,17 @@ test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytes | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django-debug-toolbar" | 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." | description = "A configurable set of panels that display various debug information about the current request/response." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"}, |     {file = "django_debug_toolbar-4.4.5-py3-none-any.whl", hash = "sha256:91425606673ee674d780f7aeedf3595c264eb382dcf41f55c6779577900904c0"}, | ||||||
|     {file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"}, |     {file = "django_debug_toolbar-4.4.5.tar.gz", hash = "sha256:8298ce966b4c8fc71430082dd4739ef2badb5f867734e1973a413c4ab2ea81b7"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| django = ">=3.2.4" | django = ">=4.2.9" | ||||||
| sqlparse = ">=0.2" | sqlparse = ">=0.2" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -508,6 +508,20 @@ packaging = "*" | |||||||
| elasticsearch = ["elasticsearch (>=5,<8)"] | elasticsearch = ["elasticsearch (>=5,<8)"] | ||||||
| testing = ["coverage", "geopy (==2)", "pysolr (>=3.7)", "python-dateutil", "requests", "whoosh (>=2.5.4,<3.0)"] | 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]] | [[package]] | ||||||
| name = "django-jinja" | name = "django-jinja" | ||||||
| version = "2.11.0" | version = "2.11.0" | ||||||
| @@ -669,13 +683,13 @@ python-dateutil = ">=2.7" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "identify" | name = "identify" | ||||||
| version = "2.5.36" | version = "2.6.0" | ||||||
| description = "File identification library for Python" | description = "File identification library for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, |     {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, | ||||||
|     {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, |     {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.extras] | [package.extras] | ||||||
| @@ -1867,4 +1881,4 @@ filelock = ">=3.4" | |||||||
| [metadata] | [metadata] | ||||||
| lock-version = "2.0" | lock-version = "2.0" | ||||||
| python-versions = "^3.10" | python-versions = "^3.10" | ||||||
| content-hash = "8524ed5f593973edf05b3c01010c1f2345b7e799089c3e38274304bdedf8b3cb" | content-hash = "51820883f41bdf40f00296b722ebdd9ac386e43ef1424ef990b29bac579ecbab" | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ django-countries = "^7.5.1" | |||||||
| dict2xml = "^1.7.3" | dict2xml = "^1.7.3" | ||||||
| Sphinx = "^5" # Needed for building xapian | Sphinx = "^5" # Needed for building xapian | ||||||
| tomli = "^2.0.1" | tomli = "^2.0.1" | ||||||
|  | django-honeypot = "^1.2.0" | ||||||
|  |  | ||||||
| [tool.poetry.group.dev.dependencies] | [tool.poetry.group.dev.dependencies] | ||||||
| django-debug-toolbar = "^4.0.0" | django-debug-toolbar = "^4.0.0" | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| # | # | ||||||
| # Copyright 2016,2017 | # Copyright 2016,2017,2024 | ||||||
| # - Skia <skia@libskia.so> | # - Skia <skia@libskia.so> | ||||||
| # - Sli <antoine@bartuccio.fr> | # - Sli <antoine@bartuccio.fr> | ||||||
| # | # | ||||||
| @@ -17,7 +17,7 @@ | |||||||
| # details. | # details. | ||||||
| # | # | ||||||
| # You should have received a copy of the GNU General Public License along with | # 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. | # 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! | # SECURITY WARNING: keep the secret key used in production secret! | ||||||
| SECRET_KEY = "(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2" | 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! | # SECURITY WARNING: don't run with debug turned on in production! | ||||||
| DEBUG = False | DEBUG = False | ||||||
| TESTING = "pytest" in sys.modules | TESTING = "pytest" in sys.modules | ||||||
| @@ -75,6 +79,7 @@ INSTALLED_APPS = ( | |||||||
|     "django.contrib.messages", |     "django.contrib.messages", | ||||||
|     "django.contrib.staticfiles", |     "django.contrib.staticfiles", | ||||||
|     "django.contrib.sites", |     "django.contrib.sites", | ||||||
|  |     "honeypot", | ||||||
|     "django_jinja", |     "django_jinja", | ||||||
|     "rest_framework", |     "rest_framework", | ||||||
|     "ajax_select", |     "ajax_select", | ||||||
| @@ -143,6 +148,7 @@ TEMPLATES = [ | |||||||
|                 "django_jinja.builtins.extensions.UrlsExtension", |                 "django_jinja.builtins.extensions.UrlsExtension", | ||||||
|                 "django_jinja.builtins.extensions.StaticFilesExtension", |                 "django_jinja.builtins.extensions.StaticFilesExtension", | ||||||
|                 "django_jinja.builtins.extensions.DjangoFiltersExtension", |                 "django_jinja.builtins.extensions.DjangoFiltersExtension", | ||||||
|  |                 "core.templatetags.extensions.HoneypotExtension", | ||||||
|             ], |             ], | ||||||
|             "filters": { |             "filters": { | ||||||
|                 "markdown": "core.templatetags.renderer.markdown", |                 "markdown": "core.templatetags.renderer.markdown", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user