factors: -> stage
This commit is contained in:
		
							
								
								
									
										0
									
								
								passbook/stages/email/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/email/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										35
									
								
								passbook/stages/email/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								passbook/stages/email/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| """EmailStage API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from passbook.stages.email.models import EmailStage | ||||
|  | ||||
|  | ||||
| class EmailStageSerializer(ModelSerializer): | ||||
|     """EmailStage Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = EmailStage | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "host", | ||||
|             "port", | ||||
|             "username", | ||||
|             "password", | ||||
|             "use_tls", | ||||
|             "use_ssl", | ||||
|             "timeout", | ||||
|             "from_address", | ||||
|             "ssl_keyfile", | ||||
|             "ssl_certfile", | ||||
|         ] | ||||
|         extra_kwargs = {"password": {"write_only": True}} | ||||
|  | ||||
|  | ||||
| class EmailStageViewSet(ModelViewSet): | ||||
|     """EmailStage Viewset""" | ||||
|  | ||||
|     queryset = EmailStage.objects.all() | ||||
|     serializer_class = EmailStageSerializer | ||||
							
								
								
									
										15
									
								
								passbook/stages/email/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								passbook/stages/email/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| """passbook email stage config""" | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class PassbookStageEmailConfig(AppConfig): | ||||
|     """passbook email stage config""" | ||||
|  | ||||
|     name = "passbook.stages.email" | ||||
|     label = "passbook_stages_email" | ||||
|     verbose_name = "passbook Stages.Email" | ||||
|  | ||||
|     def ready(self): | ||||
|         import_module("passbook.stages.email.tasks") | ||||
							
								
								
									
										40
									
								
								passbook/stages/email/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								passbook/stages/email/forms.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| """passbook administration forms""" | ||||
| from django import forms | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from passbook.stages.email.models import EmailStage | ||||
|  | ||||
|  | ||||
| class EmailStageForm(forms.ModelForm): | ||||
|     """Form to create/edit Dummy Stage""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = EmailStage | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "host", | ||||
|             "port", | ||||
|             "username", | ||||
|             "password", | ||||
|             "use_tls", | ||||
|             "use_ssl", | ||||
|             "timeout", | ||||
|             "from_address", | ||||
|             "ssl_keyfile", | ||||
|             "ssl_certfile", | ||||
|         ] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "host": forms.TextInput(), | ||||
|             "username": forms.TextInput(), | ||||
|             "password": forms.TextInput(), | ||||
|             "ssl_keyfile": forms.TextInput(), | ||||
|             "ssl_certfile": forms.TextInput(), | ||||
|         } | ||||
|         labels = { | ||||
|             "use_tls": _("Use TLS"), | ||||
|             "use_ssl": _("Use SSL"), | ||||
|             "ssl_keyfile": _("SSL Keyfile (optional)"), | ||||
|             "ssl_certfile": _("SSL Certfile (optional)"), | ||||
|         } | ||||
							
								
								
									
										50
									
								
								passbook/stages/email/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								passbook/stages/email/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| # Generated by Django 3.0.3 on 2020-05-08 17:59 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     initial = True | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_flows", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="EmailStage", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "stage_ptr", | ||||
|                     models.OneToOneField( | ||||
|                         auto_created=True, | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         parent_link=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         to="passbook_flows.Stage", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("host", models.TextField(default="localhost")), | ||||
|                 ("port", models.IntegerField(default=25)), | ||||
|                 ("username", models.TextField(blank=True, default="")), | ||||
|                 ("password", models.TextField(blank=True, default="")), | ||||
|                 ("use_tls", models.BooleanField(default=False)), | ||||
|                 ("use_ssl", models.BooleanField(default=False)), | ||||
|                 ("timeout", models.IntegerField(default=10)), | ||||
|                 ("ssl_keyfile", models.TextField(blank=True, default=None, null=True)), | ||||
|                 ("ssl_certfile", models.TextField(blank=True, default=None, null=True)), | ||||
|                 ( | ||||
|                     "from_address", | ||||
|                     models.EmailField(default="system@passbook.local", max_length=254), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Email Stage", | ||||
|                 "verbose_name_plural": "Email Stages", | ||||
|             }, | ||||
|             bases=("passbook_flows.stage",), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								passbook/stages/email/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/email/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										49
									
								
								passbook/stages/email/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								passbook/stages/email/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| """email stage models""" | ||||
| from django.core.mail.backends.smtp import EmailBackend | ||||
| from django.db import models | ||||
| from django.utils.translation import gettext as _ | ||||
|  | ||||
| from passbook.flows.models import Stage | ||||
|  | ||||
|  | ||||
| class EmailStage(Stage): | ||||
|     """email stage""" | ||||
|  | ||||
|     host = models.TextField(default="localhost") | ||||
|     port = models.IntegerField(default=25) | ||||
|     username = models.TextField(default="", blank=True) | ||||
|     password = models.TextField(default="", blank=True) | ||||
|     use_tls = models.BooleanField(default=False) | ||||
|     use_ssl = models.BooleanField(default=False) | ||||
|     timeout = models.IntegerField(default=10) | ||||
|  | ||||
|     ssl_keyfile = models.TextField(default=None, blank=True, null=True) | ||||
|     ssl_certfile = models.TextField(default=None, blank=True, null=True) | ||||
|  | ||||
|     from_address = models.EmailField(default="system@passbook.local") | ||||
|  | ||||
|     type = "passbook.stages.email.stage.EmailStageView" | ||||
|     form = "passbook.stages.email.forms.EmailStageForm" | ||||
|  | ||||
|     @property | ||||
|     def backend(self) -> EmailBackend: | ||||
|         """Get fully configured EMail Backend instance""" | ||||
|         return EmailBackend( | ||||
|             host=self.host, | ||||
|             port=self.port, | ||||
|             username=self.username, | ||||
|             password=self.password, | ||||
|             use_tls=self.use_tls, | ||||
|             use_ssl=self.use_ssl, | ||||
|             timeout=self.timeout, | ||||
|             ssl_certfile=self.ssl_certfile, | ||||
|             ssl_keyfile=self.ssl_keyfile, | ||||
|         ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"Email Stage {self.name}" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Email Stage") | ||||
|         verbose_name_plural = _("Email Stages") | ||||
							
								
								
									
										50
									
								
								passbook/stages/email/stage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								passbook/stages/email/stage.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| """passbook multi-stage authentication engine""" | ||||
| from django.contrib import messages | ||||
| from django.http import HttpRequest | ||||
| from django.shortcuts import reverse | ||||
| from django.utils.translation import gettext as _ | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.core.models import Nonce | ||||
| from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from passbook.flows.stage import AuthenticationStage | ||||
| from passbook.lib.config import CONFIG | ||||
| from passbook.stages.email.tasks import send_mails | ||||
| from passbook.stages.email.utils import TemplateEmailMessage | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class EmailStageView(AuthenticationStage): | ||||
|     """E-Mail stage which sends E-Mail for verification""" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs["show_password_forget_notice"] = CONFIG.y( | ||||
|             "passbook.password_reset.enabled" | ||||
|         ) | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] | ||||
|         nonce = Nonce.objects.create(user=pending_user) | ||||
|         # Send mail to user | ||||
|         message = TemplateEmailMessage( | ||||
|             subject=_("Forgotten password"), | ||||
|             template_name="email/account_password_reset.html", | ||||
|             to=[pending_user.email], | ||||
|             template_context={ | ||||
|                 "url": self.request.build_absolute_uri( | ||||
|                     reverse( | ||||
|                         "passbook_core:auth-password-reset", | ||||
|                         kwargs={"nonce": nonce.uuid}, | ||||
|                     ) | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|         send_mails(self.executor.current_stage, message) | ||||
|         messages.success(request, _("Check your E-Mails for a password reset link.")) | ||||
|         return self.executor.cancel() | ||||
|  | ||||
|     def post(self, request: HttpRequest): | ||||
|         """Just redirect to next stage""" | ||||
|         return self.executor.stage_ok() | ||||
							
								
								
									
										44
									
								
								passbook/stages/email/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								passbook/stages/email/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| """email stage tasks""" | ||||
| from smtplib import SMTPException | ||||
| from typing import Any, Dict, List | ||||
|  | ||||
| from celery import group | ||||
| from django.core.mail import EmailMessage | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.root.celery import CELERY_APP | ||||
| from passbook.stages.email.models import EmailStage | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def send_mails(stage: EmailStage, *messages: List[EmailMessage]): | ||||
|     """Wrapper to convert EmailMessage to dict and send it from worker""" | ||||
|     tasks = [] | ||||
|     for message in messages: | ||||
|         tasks.append(_send_mail_task.s(stage.pk, message.__dict__)) | ||||
|     lazy_group = group(*tasks) | ||||
|     promise = lazy_group() | ||||
|     return promise | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True) | ||||
| def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]): | ||||
|     """Send E-Mail according to EmailStage parameters from background worker. | ||||
|     Automatically retries if message couldn't be sent.""" | ||||
|     stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk) | ||||
|     backend = stage.backend | ||||
|     backend.open() | ||||
|     # Since django's EmailMessage objects are not JSON serialisable, | ||||
|     # we need to rebuild them from a dict | ||||
|     message_object = EmailMessage() | ||||
|     for key, value in message.items(): | ||||
|         setattr(message_object, key, value) | ||||
|     message_object.from_email = stage.from_address | ||||
|     LOGGER.debug("Sending mail", to=message_object.to) | ||||
|     try: | ||||
|         num_sent = stage.backend.send_messages([message_object]) | ||||
|     except SMTPException as exc: | ||||
|         raise self.retry(exc=exc) | ||||
|     if num_sent != 1: | ||||
|         raise self.retry() | ||||
							
								
								
									
										41
									
								
								passbook/stages/email/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								passbook/stages/email/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| """email utils""" | ||||
| from django.core.mail import EmailMultiAlternatives | ||||
| from django.template.loader import render_to_string | ||||
| from django.utils.html import strip_tags | ||||
|  | ||||
|  | ||||
| class TemplateEmailMessage(EmailMultiAlternatives): | ||||
|     """Wrapper around EmailMultiAlternatives with integrated template rendering""" | ||||
|  | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def __init__( | ||||
|         self, | ||||
|         subject="", | ||||
|         body=None, | ||||
|         from_email=None, | ||||
|         to=None, | ||||
|         bcc=None, | ||||
|         connection=None, | ||||
|         attachments=None, | ||||
|         headers=None, | ||||
|         cc=None, | ||||
|         reply_to=None, | ||||
|         template_name=None, | ||||
|         template_context=None, | ||||
|     ): | ||||
|         html_content = render_to_string(template_name, template_context) | ||||
|         if not body: | ||||
|             body = strip_tags(html_content) | ||||
|         super().__init__( | ||||
|             subject=subject, | ||||
|             body=body, | ||||
|             from_email=from_email, | ||||
|             to=to, | ||||
|             bcc=bcc, | ||||
|             connection=connection, | ||||
|             attachments=attachments, | ||||
|             headers=headers, | ||||
|             cc=cc, | ||||
|             reply_to=reply_to, | ||||
|         ) | ||||
|         self.attach_alternative(html_content, "text/html") | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer