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:
parent
2addf71f37
commit
2210497569
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 a new issue