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

20220112 email verified #591

Merged
merged 16 commits into from
Aug 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Production:

Storage
-------
All important data is stored in the databases managed by the `db*` containers. They use Docker volumes which, by default, reside in `/var/lib/docker/volumes/desec-stack_{dbapi_postgres,dblord_mysql,dbmaster_mysql}`.
All important data is stored in the databases managed by the `db*` containers. They use Docker volumes which, by default, reside in `/var/lib/docker/volumes/desec-stack_{dbapi_postgres,dblord_mysql,dbmaster_postgres}`.
This is the location you will want to back up. (Be sure to follow standard MySQL/Postgres backup practices, i.e. make sure things are consistent.)

API Versions and Roadmap
Expand Down
1 change: 1 addition & 0 deletions api/.dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
venv/
__pycache__/
4 changes: 2 additions & 2 deletions api/desecapi/authentication.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import base64
from datetime import datetime, timezone
import datetime
from ipaddress import ip_address

from django.contrib.auth.hashers import PBKDF2PasswordHasher
Expand Down Expand Up @@ -161,7 +161,7 @@ def authenticate_credentials(self, context):
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']

email_verified = datetime.fromtimestamp(serializer.timestamp, timezone.utc)
email_verified = datetime.datetime.fromtimestamp(serializer.timestamp, datetime.timezone.utc)
user.email_verified = max(user.email_verified or email_verified, email_verified)
user.save()

Expand Down
63 changes: 63 additions & 0 deletions api/desecapi/management/commands/outreach-email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import argparse
import sys

from django.core.exceptions import ImproperlyConfigured
from django.core.management import BaseCommand
from django.template import engines
from django.template.backends.django import DjangoTemplates
from django.urls import resolve, reverse

from desecapi.models import User


def _get_default_template_backend():
# Ad-hoc implementation of https://github.com/django/django/pull/15944
for backend in engines.all():
if isinstance(backend, DjangoTemplates):
return backend
raise ImproperlyConfigured("No DjangoTemplates backend is configured.")


class Command(BaseCommand):
help = 'Reach out to users with an email. Takes email template on stdin.'

def add_arguments(self, parser):
parser.add_argument('email', nargs='*', help='User(s) to contact, identified by their email addresses. '
'Defaults to everyone with outreach_preference = True.')
parser.add_argument('--contentfile', nargs='?', type=argparse.FileType('r'), default=sys.stdin,
help='File to take email content from. Defaults to stdin.')
parser.add_argument('--reason', nargs='?', default='change-outreach-preference',
help='Kind of message to send. Choose from reasons given in serializers.py. Defaults to '
'newsletter with unsubscribe link (reason: change-outreach-preference).')
parser.add_argument('--subject', nargs='?', default=None, help='Subject, default according to "reason".')

def handle(self, *args, **options):
reason = options['reason']
path = reverse(f'v1:confirm-{reason}', args=['code'])
serializer_class = resolve(path).func.cls.serializer_class

content = options['contentfile'].read().strip()
if not content and options['contentfile'].name != '/dev/null':
raise RuntimeError('Empty content only allowed from /dev/null')

try:
subject = '[deSEC] ' + options['subject']
except TypeError:
subject = None

base_file = f'emails/{reason}/content.txt'
template_code = ('{%% extends "%s" %%}' % base_file)
if content:
template_code += '{% block content %}' + content + '{% endblock %}'
template = _get_default_template_backend().from_string(template_code)

if options['email']:
users = User.objects.filter(email__in=options['email'])
elif content:
users = User.objects.filter(outreach_preference=True)
else:
raise RuntimeError('To send default content, specify recipients explicitly.')

for user in users:
action = serializer_class.Meta.model(user=user)
serializer_class(action).save(subject=subject, template=template)
7 changes: 4 additions & 3 deletions api/desecapi/management/commands/scavenge-unused.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,17 @@ def warn_domain_deletion(cls, renewal_state, notice_days, inactive_days):
domain_user_map[domain.owner].append(domain)

# Prepare and send emails, and keep renewal status in sync
deletion_date = timezone.localdate() + datetime.timedelta(days=notice_days)
context = {'deletion_date': timezone.localdate() + datetime.timedelta(days=notice_days)}
for user, domains in domain_user_map.items():
with transaction.atomic():
# Update renewal status of the user's affected domains, but don't commit before sending the email
actions = []
for domain in domains:
domain.renewal_state += 1
domain.renewal_changed = timezone.now()
domain.save(update_fields=['renewal_state', 'renewal_changed'])
domains = [{'domain': domain} for domain in domains]
user.send_confirmation_email('renew-domain', deletion_date=deletion_date, params=domains)
actions.append(models.AuthenticatedRenewDomainBasicUserAction(user=user, domain=domain))
serializers.AuthenticatedRenewDomainBasicUserActionSerializer(actions, many=True, context=context).save()

@classmethod
def delete_domains(cls, inactive_days):
Expand Down
18 changes: 18 additions & 0 deletions api/desecapi/migrations/0022_user_outreach_preference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.0.1 on 2022-01-11 19:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('desecapi', '0021_authenticatednoopuseraction'),
]

operations = [
migrations.AddField(
model_name='user',
name='outreach_preference',
field=models.BooleanField(default=True),
),
]
24 changes: 24 additions & 0 deletions api/desecapi/migrations/0023_authenticatedemailuseraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.0.1 on 2022-01-19 16:00

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('desecapi', '0022_user_outreach_preference'),
]

operations = [
migrations.CreateModel(
name='AuthenticatedEmailUserAction',
fields=[
('authenticatedbasicuseraction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='desecapi.authenticatedbasicuseraction')),
],
options={
'managed': False,
},
bases=('desecapi.authenticatedbasicuseraction',),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.0.1 on 2022-01-20 11:21

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('desecapi', '0023_authenticatedemailuseraction'),
]

operations = [
migrations.CreateModel(
name='AuthenticatedChangeOutreachPreferenceUserAction',
fields=[
('authenticatedemailuseraction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='desecapi.authenticatedemailuseraction')),
('outreach_preference', models.BooleanField()),
],
options={
'managed': False,
},
bases=('desecapi.authenticatedemailuseraction',),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.0.6 on 2022-08-11 16:37

import datetime
import django.core.validators
from django.db import migrations, models


def forwards_func(apps, schema_editor):
max_interval = datetime.timedelta(days=365000)
Token = apps.get_model("desecapi", "Token")
db_alias = schema_editor.connection.alias
Token.objects.using(db_alias).filter(max_age__gt=max_interval).update(max_age=max_interval)
Token.objects.using(db_alias).filter(max_unused_period__gt=max_interval).update(max_unused_period=max_interval)


class Migration(migrations.Migration):

dependencies = [
('desecapi', '0024_authenticatedchangeoutreachpreferenceuseraction'),
]

operations = [
migrations.AlterField(
model_name='token',
name='max_age',
field=models.DurationField(default=None, null=True, validators=[django.core.validators.MinValueValidator(datetime.timedelta(0)), django.core.validators.MaxValueValidator(datetime.timedelta(days=365000))]),
),
migrations.AlterField(
model_name='token',
name='max_unused_period',
field=models.DurationField(default=None, null=True, validators=[django.core.validators.MinValueValidator(datetime.timedelta(0)), django.core.validators.MaxValueValidator(datetime.timedelta(days=365000))]),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]
Loading