From c3c664c402bd6142ffae39872bb379dcc211ca26 Mon Sep 17 00:00:00 2001 From: chriscummings Date: Wed, 11 Jun 2025 09:22:49 -0500 Subject: [PATCH 1/8] ci:test main From 0f9f825043568918a26ab41e07129460341381ab Mon Sep 17 00:00:00 2001 From: chriscummings Date: Wed, 11 Jun 2025 09:49:05 -0500 Subject: [PATCH 2/8] fix(pre-commit): apparently the config syntax has changed --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08d8da08..f5bc8d3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: 'docs|migrations|.git|.tox' -default_stages: [commit] +default_stages: [pre-commit] fail_fast: true repos: From ff4fb7feb846c21af972a2147857146be961cc83 Mon Sep 17 00:00:00 2001 From: chriscummings Date: Wed, 11 Jun 2025 09:51:55 -0500 Subject: [PATCH 3/8] fix(default password): make default password use non-deprecated function and unspecial chars --- scram/route_manager/views.py | 6 ++---- scram/shared/__init__.py | 1 + scram/shared/shared_code.py | 27 +++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 scram/shared/__init__.py create mode 100644 scram/shared/shared_code.py diff --git a/scram/route_manager/views.py b/scram/route_manager/views.py index 50abbd7f..7353d9ff 100644 --- a/scram/route_manager/views.py +++ b/scram/route_manager/views.py @@ -24,6 +24,7 @@ from scram.route_manager.models import WebSocketSequenceElement from ..route_manager.api.views import EntryViewSet +from ..shared.code import make_random_password from ..users.models import User from .models import ActionType, Entry @@ -50,10 +51,7 @@ def home_page(request, prefilter=None): if settings.AUTOCREATE_ADMIN: if User.objects.count() == 0: - password = User.objects.make_random_password( - length=20, - allowed_chars="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*", - ) + password = make_random_password(length=20) User.objects.create_superuser("admin", "admin@example.com", password) authenticated_admin = authenticate(request, username="admin", password=password) login(request, authenticated_admin) diff --git a/scram/shared/__init__.py b/scram/shared/__init__.py new file mode 100644 index 00000000..9af65b5d --- /dev/null +++ b/scram/shared/__init__.py @@ -0,0 +1 @@ +"""A Module for defining shared code and constants.""" diff --git a/scram/shared/shared_code.py b/scram/shared/shared_code.py new file mode 100644 index 00000000..d098b9b7 --- /dev/null +++ b/scram/shared/shared_code.py @@ -0,0 +1,27 @@ +"""A Module for defining shared code.""" + +import secrets +import string + + +def make_random_password(length: int = 10, min_digits: int = 3) -> str: + """make_random_password django deprecated this function so let's roll our own, wcgw. + + Args: + length (int, optional): The password length. Defaults to 10. + min_digits (int, optional): The minimum number of digits. Defaults to 3. + + Returns: + str: The generated password. + """ + # todo: add sanity checks for length and min_digits, i.e. min_digits <= length, and do something about long + # passwords which will prolly take forever. + alphabet = string.ascii_letters + string.digits + while True: + password = "".join(secrets.choice(alphabet) for i in range(length)) + if ( + any(c.islower() for c in password) + and any(c.isupper() for c in password) + and sum(c.isdigit() for c in password) >= min_digits + ): + return password From 82bd9b37a8e4243d1fbc867850836c517bbe335d Mon Sep 17 00:00:00 2001 From: chriscummings Date: Wed, 11 Jun 2025 09:52:14 -0500 Subject: [PATCH 4/8] fix(default password): dagnabbit, use the right thing --- scram/route_manager/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scram/route_manager/views.py b/scram/route_manager/views.py index 7353d9ff..d6e341db 100644 --- a/scram/route_manager/views.py +++ b/scram/route_manager/views.py @@ -24,7 +24,7 @@ from scram.route_manager.models import WebSocketSequenceElement from ..route_manager.api.views import EntryViewSet -from ..shared.code import make_random_password +from ..shared.shared_code import make_random_password from ..users.models import User from .models import ActionType, Entry From 39538f0ddf8b062fc9913f65c9cff9844a8ee0d7 Mon Sep 17 00:00:00 2001 From: chriscummings Date: Thu, 12 Jun 2025 15:04:48 -0500 Subject: [PATCH 5/8] refactor(make_random_password): add sanity checks and error handling. --- scram/shared/shared_code.py | 50 ++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/scram/shared/shared_code.py b/scram/shared/shared_code.py index d098b9b7..926d7bb7 100644 --- a/scram/shared/shared_code.py +++ b/scram/shared/shared_code.py @@ -4,24 +4,56 @@ import string -def make_random_password(length: int = 10, min_digits: int = 3) -> str: - """make_random_password django deprecated this function so let's roll our own, wcgw. +def make_random_password(length: int = 20, min_digits: int = 5, max_attempts: int = 10000) -> str: + """make_random_password replaces the deprecated django make_random_password function. + + Generates a random password of a specified length containing at least a specified number of digits using the + official python best practices and some additional sanity checks. The python docs for this can be found at + https://docs.python.org/3/library/secrets.html#recipes-and-best-practices. Note that generating long passwords + with a high number of digits (>100) is inefficient and should be avoided. This password should only be used for + temporary purposes, such as for a user to log in to the web interface and change their password. Args: - length (int, optional): The password length. Defaults to 10. - min_digits (int, optional): The minimum number of digits. Defaults to 3. + length (int, optional): The total length of the password to generate. Defaults to 20. + min_digits (int, optional): The minimum number of digits the password needs. Defaults to 5. + max_attempts (int, optional): The maximum number of attempts to generate a valid password. Defaults to 10000. + + Raises: + ValueError: Password length must be at least 1 + ValueError: min_digits cannot be negative + ValueError: min_digits cannot exceed password length + ValueError: For performance reasons, min_digits cannot exceed 30% of the password length + RuntimeError: Failed to generate a valid password after max_attempts attempts Returns: - str: The generated password. + password (str): The generated password. """ - # todo: add sanity checks for length and min_digits, i.e. min_digits <= length, and do something about long - # passwords which will prolly take forever. + if length < 1: + message = "Password length must be at least 1" + raise ValueError(message) + if min_digits < 0: + message = "min_digits cannot be negative" + raise ValueError(message) + if min_digits > length: + message = "min_digits cannot exceed password length" + raise ValueError(message) + # Only allow a somewhat arbitrary threshold of 30% of the password length for min_digits, for performance reasons. + if min_digits > length * 0.3: + message = "For performance reasons, min_digits cannot exceed 30% of the password length" + raise ValueError(message) + alphabet = string.ascii_letters + string.digits - while True: - password = "".join(secrets.choice(alphabet) for i in range(length)) + + for _attempt in range(max_attempts): + password = "".join(secrets.choice(alphabet) for _i in range(length)) if ( any(c.islower() for c in password) and any(c.isupper() for c in password) and sum(c.isdigit() for c in password) >= min_digits ): return password + + # If we reached this point, we failed to generate a valid password after max_attempts attempts, likely due to the + # required password length being very long and the min_digits being high. + message = f"Failed to generate a valid password after {max_attempts} attempts" + raise RuntimeError(message) From 5a247e52f7a27c4754c5ce6fca8fb6f19ca98189 Mon Sep 17 00:00:00 2001 From: chriscummings Date: Thu, 12 Jun 2025 15:27:30 -0500 Subject: [PATCH 6/8] fix(settings): Remove MAOR special character crud. --- config/settings/local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/local.py b/config/settings/local.py index 6025087f..953d45c6 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -12,7 +12,7 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key SECRET_KEY = env( "DJANGO_SECRET_KEY", - default=get_random_secret_key(), + default=get_random_secret_key(chars="abcdefghijklmnopqrstuvwxyz0123456789"), ) # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ["*"] From 98273c95a387e320aa49623536db1509705a2bc1 Mon Sep 17 00:00:00 2001 From: chriscummings Date: Thu, 12 Jun 2025 15:31:34 -0500 Subject: [PATCH 7/8] fix(secrets): derp, forgot to update the function we're calling --- config/settings/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings/local.py b/config/settings/local.py index 953d45c6..7af8b14a 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -1,6 +1,6 @@ """Use these settings for local development.""" -from django.core.management.utils import get_random_secret_key +from django.core.management.utils import get_random_string from .base import * # noqa from .base import AUTH_METHOD, REST_FRAMEWORK, env @@ -12,7 +12,7 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key SECRET_KEY = env( "DJANGO_SECRET_KEY", - default=get_random_secret_key(chars="abcdefghijklmnopqrstuvwxyz0123456789"), + default=get_random_string(chars="abcdefghijklmnopqrstuvwxyz0123456789"), ) # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ["*"] From 9354b68eb913f993c1d195713be80bf9d0b060ca Mon Sep 17 00:00:00 2001 From: chriscummings Date: Thu, 12 Jun 2025 15:45:27 -0500 Subject: [PATCH 8/8] fix(secrets): and this time I mean it (calling the right function) --- config/settings/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings/local.py b/config/settings/local.py index 7af8b14a..c6618ab3 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -1,6 +1,6 @@ """Use these settings for local development.""" -from django.core.management.utils import get_random_string +from django.utils.crypto import get_random_string from .base import * # noqa from .base import AUTH_METHOD, REST_FRAMEWORK, env @@ -12,7 +12,7 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key SECRET_KEY = env( "DJANGO_SECRET_KEY", - default=get_random_string(chars="abcdefghijklmnopqrstuvwxyz0123456789"), + default=get_random_string(50, allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789"), ) # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ["*"]