Skip to content
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
exclude: 'docs|migrations|.git|.tox'
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: true

repos:
Expand Down
4 changes: 2 additions & 2 deletions config/settings/local.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Use these settings for local development."""

from django.core.management.utils import get_random_secret_key
from django.utils.crypto import get_random_string

from .base import * # noqa
from .base import AUTH_METHOD, REST_FRAMEWORK, env
Expand All @@ -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_string(50, allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789"),
)
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["*"]
Expand Down
6 changes: 2 additions & 4 deletions scram/route_manager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from scram.route_manager.models import WebSocketSequenceElement

from ..route_manager.api.views import EntryViewSet
from ..shared.shared_code import make_random_password
from ..users.models import User
from .models import ActionType, Entry

Expand All @@ -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)
Comment thread
samoehlert marked this conversation as resolved.
User.objects.create_superuser("admin", "admin@example.com", password)
authenticated_admin = authenticate(request, username="admin", password=password)
login(request, authenticated_admin)
Expand Down
1 change: 1 addition & 0 deletions scram/shared/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""A Module for defining shared code and constants."""
59 changes: 59 additions & 0 deletions scram/shared/shared_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""A Module for defining shared code."""

import secrets
import string


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 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:
password (str): The generated password.
"""
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

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)
Loading