events: add EMAIL_SENT event, show sent emails in event log
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		
							
								
								
									
										45
									
								
								authentik/events/migrations/0015_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								authentik/events/migrations/0015_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| # Generated by Django 3.2.3 on 2021-06-09 07:58 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0014_expiry"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="event", | ||||
|             name="action", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("login", "Login"), | ||||
|                     ("login_failed", "Login Failed"), | ||||
|                     ("logout", "Logout"), | ||||
|                     ("user_write", "User Write"), | ||||
|                     ("suspicious_request", "Suspicious Request"), | ||||
|                     ("password_set", "Password Set"), | ||||
|                     ("secret_view", "Secret View"), | ||||
|                     ("invitation_used", "Invite Used"), | ||||
|                     ("authorize_application", "Authorize Application"), | ||||
|                     ("source_linked", "Source Linked"), | ||||
|                     ("impersonation_started", "Impersonation Started"), | ||||
|                     ("impersonation_ended", "Impersonation Ended"), | ||||
|                     ("policy_execution", "Policy Execution"), | ||||
|                     ("policy_exception", "Policy Exception"), | ||||
|                     ("property_mapping_exception", "Property Mapping Exception"), | ||||
|                     ("system_task_execution", "System Task Execution"), | ||||
|                     ("system_task_exception", "System Task Exception"), | ||||
|                     ("configuration_error", "Configuration Error"), | ||||
|                     ("model_created", "Model Created"), | ||||
|                     ("model_updated", "Model Updated"), | ||||
|                     ("model_deleted", "Model Deleted"), | ||||
|                     ("email_sent", "Email Sent"), | ||||
|                     ("update_available", "Update Available"), | ||||
|                     ("custom_", "Custom Prefix"), | ||||
|                 ] | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -77,6 +77,7 @@ class EventAction(models.TextChoices): | ||||
|     MODEL_CREATED = "model_created" | ||||
|     MODEL_UPDATED = "model_updated" | ||||
|     MODEL_DELETED = "model_deleted" | ||||
|     EMAIL_SENT = "email_sent" | ||||
|  | ||||
|     UPDATE_AVAILABLE = "update_available" | ||||
|  | ||||
|  | ||||
| @ -0,0 +1,47 @@ | ||||
| # Generated by Django 3.2.3 on 2021-06-09 07:58 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_policies_event_matcher", "0015_alter_eventmatcherpolicy_app"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="eventmatcherpolicy", | ||||
|             name="action", | ||||
|             field=models.TextField( | ||||
|                 blank=True, | ||||
|                 choices=[ | ||||
|                     ("login", "Login"), | ||||
|                     ("login_failed", "Login Failed"), | ||||
|                     ("logout", "Logout"), | ||||
|                     ("user_write", "User Write"), | ||||
|                     ("suspicious_request", "Suspicious Request"), | ||||
|                     ("password_set", "Password Set"), | ||||
|                     ("secret_view", "Secret View"), | ||||
|                     ("invitation_used", "Invite Used"), | ||||
|                     ("authorize_application", "Authorize Application"), | ||||
|                     ("source_linked", "Source Linked"), | ||||
|                     ("impersonation_started", "Impersonation Started"), | ||||
|                     ("impersonation_ended", "Impersonation Ended"), | ||||
|                     ("policy_execution", "Policy Execution"), | ||||
|                     ("policy_exception", "Policy Exception"), | ||||
|                     ("property_mapping_exception", "Property Mapping Exception"), | ||||
|                     ("system_task_execution", "System Task Execution"), | ||||
|                     ("system_task_exception", "System Task Exception"), | ||||
|                     ("configuration_error", "Configuration Error"), | ||||
|                     ("model_created", "Model Created"), | ||||
|                     ("model_updated", "Model Updated"), | ||||
|                     ("model_deleted", "Model Deleted"), | ||||
|                     ("email_sent", "Email Sent"), | ||||
|                     ("update_available", "Update Available"), | ||||
|                     ("custom_", "Custom Prefix"), | ||||
|                 ], | ||||
|                 help_text="Match created events with this action type. When left empty, all action types will be matched.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -45,6 +45,9 @@ class PolicyRequest: | ||||
|             return | ||||
|         self.context["geoip"] = GEOIP_READER.city(client_ip) | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
|         return self.__str__() | ||||
|  | ||||
|     def __str__(self): | ||||
|         text = f"<PolicyRequest user={self.user}" | ||||
|         if self.obj: | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| ------------------------------------- */ | ||||
| * { | ||||
|   margin: 0; | ||||
|   font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||||
|   font-family: Helvetica, Arial, sans-serif; | ||||
|   box-sizing: border-box; | ||||
|   font-size: 14px; | ||||
| } | ||||
| @ -91,7 +91,7 @@ body { | ||||
|     TYPOGRAPHY | ||||
| ------------------------------------- */ | ||||
| h1, h2, h3 { | ||||
|   font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; | ||||
|   font-family: Helvetica, Arial, sans-serif; | ||||
|   color: #000; | ||||
|   margin: 40px 0 0; | ||||
|   line-height: 1.2em; | ||||
|  | ||||
| @ -9,6 +9,7 @@ from django.core.mail.utils import DNS_NAME | ||||
| from django.utils.text import slugify | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.stages.email.models import EmailStage | ||||
| @ -26,6 +27,14 @@ def send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]): | ||||
|     return promise | ||||
|  | ||||
|  | ||||
| def get_email_body(email: EmailMultiAlternatives) -> str: | ||||
|     """Get the email's body. Will return HTML alt if set, otherwise plain text body""" | ||||
|     for alt_content, alt_type in email.alternatives: | ||||
|         if alt_type == "text/html": | ||||
|             return alt_content | ||||
|     return email.body | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     bind=True, | ||||
|     autoretry_for=( | ||||
| @ -68,6 +77,14 @@ def send_mail( | ||||
|  | ||||
|         LOGGER.debug("Sending mail", to=message_object.to) | ||||
|         stage.backend.send_messages([message_object]) | ||||
|         Event.new( | ||||
|             EventAction.EMAIL_SENT, | ||||
|             message=(f"Email to {', '.join(message_object.to)} sent"), | ||||
|             subject=message_object.subject, | ||||
|             body=get_email_body(message_object), | ||||
|             from_email=message_object.from_email, | ||||
|             to_email=message_object.to, | ||||
|         ).save() | ||||
|         self.set_status( | ||||
|             TaskResult( | ||||
|                 TaskResultStatus.SUCCESSFUL, | ||||
|  | ||||
| @ -8,6 +8,7 @@ from django.test import Client, TestCase | ||||
| from django.urls import reverse | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.markers import StageMarker | ||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||
| @ -55,6 +56,13 @@ class TestEmailStageSending(TestCase): | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertEqual(len(mail.outbox), 1) | ||||
|             self.assertEqual(mail.outbox[0].subject, "authentik") | ||||
|             events = Event.objects.filter(action=EventAction.EMAIL_SENT) | ||||
|             self.assertEqual(len(events), 1) | ||||
|             event = events.first() | ||||
|             self.assertEqual(event.context["message"], "Email to test@beryju.org sent") | ||||
|             self.assertEqual(event.context["subject"], "authentik") | ||||
|             self.assertEqual(event.context["to_email"], ["test@beryju.org"]) | ||||
|             self.assertEqual(event.context["from_email"], "system@authentik.local") | ||||
|  | ||||
|     def test_send_error(self): | ||||
|         """Test error during sending (sending will be retried)""" | ||||
|  | ||||
| @ -15385,6 +15385,7 @@ components: | ||||
|       - model_created | ||||
|       - model_updated | ||||
|       - model_deleted | ||||
|       - email_sent | ||||
|       - update_available | ||||
|       - custom_ | ||||
|       type: string | ||||
|  | ||||
| @ -1276,6 +1276,10 @@ msgstr "Email" | ||||
| msgid "Email address" | ||||
| msgstr "Email address" | ||||
|  | ||||
| #: src/pages/events/EventInfo.ts | ||||
| msgid "Email info:" | ||||
| msgstr "Email info:" | ||||
|  | ||||
| #: src/flows/stages/identification/IdentificationStage.ts | ||||
| msgid "Email or username" | ||||
| msgstr "Email or username" | ||||
| @ -1634,6 +1638,10 @@ msgstr "Forward auth (single application)" | ||||
| msgid "Friendly Name" | ||||
| msgstr "Friendly Name" | ||||
|  | ||||
| #: src/pages/events/EventInfo.ts | ||||
| msgid "From" | ||||
| msgstr "From" | ||||
|  | ||||
| #: src/pages/stages/email/EmailStageForm.ts | ||||
| msgid "From address" | ||||
| msgstr "From address" | ||||
| @ -2149,6 +2157,10 @@ msgstr "Maximum age (in days)" | ||||
| msgid "Members" | ||||
| msgstr "Members" | ||||
|  | ||||
| #: src/pages/events/EventInfo.ts | ||||
| msgid "Message" | ||||
| msgstr "Message" | ||||
|  | ||||
| #: src/pages/applications/ApplicationCheckAccessForm.ts | ||||
| #: src/pages/events/EventInfo.ts | ||||
| #: src/pages/policies/PolicyTestForm.ts | ||||
| @ -3391,6 +3403,7 @@ msgstr "Status: Enabled" | ||||
| msgid "Stop impersonation" | ||||
| msgstr "Stop impersonation" | ||||
|  | ||||
| #: src/pages/events/EventInfo.ts | ||||
| #: src/pages/stages/email/EmailStageForm.ts | ||||
| msgid "Subject" | ||||
| msgstr "Subject" | ||||
| @ -3865,6 +3878,10 @@ msgstr "Timeout" | ||||
| msgid "Title" | ||||
| msgstr "Title" | ||||
|  | ||||
| #: src/pages/events/EventInfo.ts | ||||
| msgid "To" | ||||
| msgstr "To" | ||||
|  | ||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts | ||||
| msgid "To use SSL instead, use 'ldaps://' and disable this option." | ||||
| msgstr "To use SSL instead, use 'ldaps://' and disable this option." | ||||
|  | ||||
| @ -1268,6 +1268,10 @@ msgstr "" | ||||
| msgid "Email address" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "Email info:" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "Email or username" | ||||
| msgstr "" | ||||
| @ -1626,6 +1630,10 @@ msgstr "" | ||||
| msgid "Friendly Name" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "From" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "From address" | ||||
| msgstr "" | ||||
| @ -2141,6 +2149,10 @@ msgstr "" | ||||
| msgid "Members" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "Message" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| #:  | ||||
| #:  | ||||
| @ -3383,6 +3395,7 @@ msgstr "" | ||||
| msgid "Stop impersonation" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| #:  | ||||
| msgid "Subject" | ||||
| msgstr "" | ||||
| @ -3853,6 +3866,10 @@ msgstr "" | ||||
| msgid "Title" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "To" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| msgid "To use SSL instead, use 'ldaps://' and disable this option." | ||||
| msgstr "" | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { t } from "@lingui/macro"; | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { until } from "lit-html/directives/until"; | ||||
| import { FlowsApi } from "authentik-api"; | ||||
| import { ActionEnum, FlowsApi } from "authentik-api"; | ||||
| import "../../elements/Spinner"; | ||||
| import "../../elements/Expand"; | ||||
| import { PFSize } from "../../elements/Spinner"; | ||||
| @ -32,6 +32,10 @@ export class EventInfo extends LitElement { | ||||
|                 .pf-l-flex__item { | ||||
|                     min-width: 25%; | ||||
|                 } | ||||
|                 iframe { | ||||
|                     width: 100%; | ||||
|                     height: 50rem; | ||||
|                 } | ||||
|             ` | ||||
|         ]; | ||||
|     } | ||||
| @ -76,6 +80,50 @@ export class EventInfo extends LitElement { | ||||
|         </dl>`; | ||||
|     } | ||||
|  | ||||
|     getEmailInfo(context: EventContext): TemplateResult { | ||||
|         if (context === null) { | ||||
|             return html`<span>-</span>`; | ||||
|         } | ||||
|         return html`<dl class="pf-c-description-list pf-m-horizontal"> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Message`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.message}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Subject`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.subject}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`From`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.from_email}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`To`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text"> | ||||
|                         ${(context.to_email as string[]).map(to => { | ||||
|                             return html`<li>${to}</li>`; | ||||
|                         })} | ||||
|                     </div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|         </dl>`; | ||||
|     } | ||||
|  | ||||
|     defaultResponse(): TemplateResult { | ||||
|         return html`<div class="pf-l-flex"> | ||||
|                 <div class="pf-l-flex__item"> | ||||
| @ -94,14 +142,14 @@ export class EventInfo extends LitElement { | ||||
|             return html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`; | ||||
|         } | ||||
|         switch (this.event?.action) { | ||||
|         case "model_created": | ||||
|         case "model_updated": | ||||
|         case "model_deleted": | ||||
|         case ActionEnum.ModelCreated: | ||||
|         case ActionEnum.ModelUpdated: | ||||
|         case ActionEnum.ModelDeleted: | ||||
|             return html` | ||||
|                 <h3>${t`Affected model:`}</h3> | ||||
|                 ${this.getModelInfo(this.event.context?.model as EventContext)} | ||||
|                 `; | ||||
|         case "authorize_application": | ||||
|         case ActionEnum.AuthorizeApplication: | ||||
|             return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${t`Authorized application:`}</h3> | ||||
| @ -118,15 +166,17 @@ export class EventInfo extends LitElement { | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case "login_failed": | ||||
|             return html` | ||||
|                 <h3>${t`Attempted to log in as ${this.event.context.username}`}</h3> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case "secret_view": | ||||
|         case ActionEnum.EmailSent: | ||||
|             return html`<h3>${t`Email info:`}</h3> | ||||
|                 ${this.getEmailInfo(this.event.context)} | ||||
|                 <ak-expand> | ||||
|                     <iframe srcdoc=${this.event.context.body}></iframe> | ||||
|                 </ak-expand>`; | ||||
|         case ActionEnum.SecretView: | ||||
|             return html` | ||||
|                 <h3>${t`Secret:`}</h3> | ||||
|                 ${this.getModelInfo(this.event.context.secret as EventContext)}`; | ||||
|         case "property_mapping_exception": | ||||
|         case ActionEnum.PropertyMappingException: | ||||
|             return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${t`Exception`}</h3> | ||||
| @ -138,7 +188,7 @@ export class EventInfo extends LitElement { | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case "policy_exception": | ||||
|         case ActionEnum.PolicyException: | ||||
|             return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${t`Binding`}</h3> | ||||
| @ -157,7 +207,7 @@ export class EventInfo extends LitElement { | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case "policy_execution": | ||||
|         case ActionEnum.PolicyExecution: | ||||
|             return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${t`Binding`}</h3> | ||||
| @ -185,16 +235,19 @@ export class EventInfo extends LitElement { | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case "configuration_error": | ||||
|         case ActionEnum.ConfigurationError: | ||||
|             return html`<h3>${this.event.context.message}</h3> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case "update_available": | ||||
|         case ActionEnum.UpdateAvailable: | ||||
|             return html`<h3>${t`New version available!`}</h3> | ||||
|                 <a target="_blank" href="https://github.com/goauthentik/authentik/releases/tag/version%2F${this.event.context.new_version}">${this.event.context.new_version}</a> | ||||
|                 `; | ||||
|                 <a | ||||
|                     target="_blank" | ||||
|                     href="https://github.com/goauthentik/authentik/releases/tag/version%2F${this.event.context.new_version}"> | ||||
|                     ${this.event.context.new_version} | ||||
|                 </a>`; | ||||
|         // Action types which typically don't record any extra context. | ||||
|         // If context is not empty, we fall to the default response. | ||||
|         case "login": | ||||
|         case ActionEnum.Login: | ||||
|             if ("using_source" in this.event.context) { | ||||
|                 return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
| @ -204,7 +257,11 @@ export class EventInfo extends LitElement { | ||||
|                 </div>`; | ||||
|             } | ||||
|             return this.defaultResponse(); | ||||
|         case "logout": | ||||
|         case ActionEnum.LoginFailed: | ||||
|             return html` | ||||
|                 <h3>${t`Attempted to log in as ${this.event.context.username}`}</h3> | ||||
|                 <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|         case ActionEnum.Logout: | ||||
|             if (this.event.context === {}) { | ||||
|                 return html`<span>${t`No additional data available.`}</span>`; | ||||
|             } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer