Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve user management security #359

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions {{cookiecutter.project_slug}}/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ SMTP_PORT='587'
SMTP_VALID_TESTING_DOMAINS='thinknimble.com'
DEFAULT_FROM_EMAIL='{{ cookiecutter.project_name }} <noreply@{{ cookiecutter.project_slug }}.com>'
{% endif %}
USE_EMAIL_ALLOWLIST=True
EMAIL_ALLOWLIST=['[email protected]']

# Testing (NOTE: Heroku and Github Actions will need to have matching values for some of these)
DJANGO_SUPERUSER_PASSWORD='!!!DJANGO_SECRET_KEY!!!'
Expand Down
7 changes: 7 additions & 0 deletions {{cookiecutter.project_slug}}/app.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "{{ cookiecutter.project_name }}",
"stack": "heroku-24",
"env": {
"ALLOWED_HOSTS": {
"value": ".herokuapp.com"
Expand Down Expand Up @@ -27,6 +28,12 @@
},
"SECRET_KEY": {
"generator": "secret"
},
"USE_EMAIL_ALLOWLIST": {
"value": "True"
},
"EMAIL_ALLOWLIST": {
"value": ["[email protected]"]
}
},
"addons": ["heroku-postgresql:standard-0", "papertrail:choklad"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import logging

from decouple import config
from django.contrib.auth import get_user_model
from django.conf import settings
from django.core.management.base import BaseCommand

from {{ cookiecutter.project_slug }}.utils.emails import send_html_email

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Alert the team on potentially important platform metrics"

def handle(self, *args, **kwargs):
logger.info(f"Starting management command {__name__}")
email_content = ""

recent_user_signups = User.objects.values("created__date").annotate(count=Count("created__date")).order_by("-created__date").values_list("created__date", "count")[0:7]
for date, count in recent_user_signups:
email_content += f"{date}: {count}</br>"

title = "{{ cookiecutter.project_name }} Platform Metrics"
context = {
"content": email_content
}
send_html_email(title, "core/metrics.html", settings.DEFAULT_FROM_EMAIL, [settings.STAFF_EMAIL], context=context)

logger.info(f"Finished management command {__name__}")
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from email.utils import parseaddr

from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers
Expand Down Expand Up @@ -61,10 +64,35 @@ class Meta:
"last_name": {"required": True},
}

def _validate_name(self, value):
"""
There are MANY unique names out there, so let users input whatever they want.
BUT...alert the devs if we see something odd.
"""
if not "".join(a.split()).isalpha():
logger.warning(f"User signup with non-alphabetic characters in their name: {value}")

def validate_first_name(self, value):
self._validate_name(value)
return value

def validate_last_name(self, value):
self._validate_name(value)
return value

def validate_email(self, value):
value = parseaddr(value, strict=True)[1].lower()
if settings.USE_EMAIL_ALLOWLIST and value not in settings.EMAIL_ALLOWLIST:
raise ValidationError(detail="Invalid email")
if not all(c in value for c in [".", "@"])
raise ValidationError(detail="Invalid email")
if not any(value.endswith(c) for c in [".com", ".net", ".org", ".co.uk"]):
logger.warning(f"Potentially risky email: {value}")
return value

def validate(self, data):
password = data.get("password")
validate_password(password)

return data

def create(self, validated_data):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{%- raw -%}
{% extends '_alert-email-base.html' %}
{% load tz %}
{% comment %}

Platform Metrics

Required context variables::

:site_url: so we can link to the site this URL came from
:support_email: email address to contact for support

Content blocks::

title - Large bolded title at the top of the message
main_content - The important message
button_link - URL for the action button on the page
button_text - the label for the action button
footer - closing thought on the message

{% endcomment %}

{% block title %}Forgot your password?{% endblock %}

{% block main_content %}
<tr>
<td class="em_grey" align="center" valign="top" style="font-family: Arial, sans-serif; font-size: 16px; line-height: 26px; color:#434343;">It happens to the best of us. The good news is you can change it right now.</td>
</tr>
{% endblock main_content %}

{% block footer %}
<tr>
<td class="em_grey" align="center" valign="top" style="font-family: Arial, sans-serif; font-size: 16px; line-height: 26px; color:#434343;">If you didn&rsquo;t request a password reset, you don&rsquo;t have to do anything.<br class="em_hide" />
Just ignore this email.</td>
</tr>
{% endblock footer %}
{%- endraw -%}
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@
else:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

USE_EMAIL_ALLOWLIST = config("USE_EMAIL_ALLOWLIST", cast=bool, default=False)
EMAIL_ALLOWLIST = config("EMAIL_ALLOWLIST", default=[])

# STORAGES
# ----------------------------------------------------------------------------

Expand Down
Loading