events: add tenant to event

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-06-14 17:34:42 +02:00
parent e584fd1344
commit 74e578c2bf
15 changed files with 233 additions and 28 deletions

View file

@ -36,6 +36,7 @@ class EventSerializer(ModelSerializer):
"client_ip", "client_ip",
"created", "created",
"expires", "expires",
"tenant",
] ]
@ -76,6 +77,11 @@ class EventsFilter(django_filters.FilterSet):
field_name="action", field_name="action",
lookup_expr="icontains", lookup_expr="icontains",
) )
tenant_name = django_filters.CharFilter(
field_name="tenant",
lookup_expr="name",
label="Tenant name",
)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def filter_context_model_pk(self, queryset, name, value): def filter_context_model_pk(self, queryset, name, value):

View file

@ -40,9 +40,9 @@ class GeoIPReader:
return return
try: try:
reader = Reader(path) reader = Reader(path)
LOGGER.info("Loaded GeoIP database")
self.__reader = reader self.__reader = reader
self.__last_mtime = stat(path).st_mtime self.__last_mtime = stat(path).st_mtime
LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime)
except OSError as exc: except OSError as exc:
LOGGER.warning("Failed to load GeoIP database", exc=exc) LOGGER.warning("Failed to load GeoIP database", exc=exc)

View file

@ -0,0 +1,55 @@
# Generated by Django 3.2.4 on 2021-06-14 15:33
from django.db import migrations, models
import authentik.events.models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0015_alter_event_action"),
]
operations = [
migrations.AddField(
model_name="event",
name="tenant",
field=models.JSONField(
blank=True, default=authentik.events.models.default_tenant
),
),
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"),
("system_exception", "System 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"),
]
),
),
]

View file

@ -21,11 +21,12 @@ from authentik.core.middleware import (
) )
from authentik.core.models import ExpiringModel, Group, User from authentik.core.models import ExpiringModel, Group, User
from authentik.events.geo import GEOIP_READER from authentik.events.geo import GEOIP_READER
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger("authentik.events") LOGGER = get_logger("authentik.events")
GAUGE_EVENTS = Gauge( GAUGE_EVENTS = Gauge(
@ -40,6 +41,11 @@ def default_event_duration():
return now() + timedelta(days=365) return now() + timedelta(days=365)
def default_tenant():
"""Get a default value for tenant"""
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
class NotificationTransportError(SentryIgnoredException): class NotificationTransportError(SentryIgnoredException):
"""Error raised when a notification fails to be delivered""" """Error raised when a notification fails to be delivered"""
@ -95,6 +101,7 @@ class Event(ExpiringModel):
context = models.JSONField(default=dict, blank=True) context = models.JSONField(default=dict, blank=True)
client_ip = models.GenericIPAddressField(null=True) client_ip = models.GenericIPAddressField(null=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
tenant = models.JSONField(default=default_tenant, blank=True)
# Shadow the expires attribute from ExpiringModel to override the default duration # Shadow the expires attribute from ExpiringModel to override the default duration
expires = models.DateTimeField(default=default_event_duration) expires = models.DateTimeField(default=default_event_duration)
@ -133,6 +140,13 @@ class Event(ExpiringModel):
"""Add data from a Django-HttpRequest, allowing the creation of """Add data from a Django-HttpRequest, allowing the creation of
Events independently from requests. Events independently from requests.
`user` arguments optionally overrides user from requests.""" `user` arguments optionally overrides user from requests."""
if request:
self.context["http_request"] = {
"path": request.get_full_path(),
"method": request.method,
}
if hasattr(request, "tenant"):
self.tenant = sanitize_dict(model_to_dict(request.tenant))
if hasattr(request, "user"): if hasattr(request, "user"):
original_user = None original_user = None
if hasattr(request, "session"): if hasattr(request, "session"):

View file

@ -0,0 +1,48 @@
# Generated by Django 3.2.4 on 2021-06-14 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_event_matcher", "0016_alter_eventmatcherpolicy_action"),
]
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"),
("system_exception", "System 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.",
),
),
]

View file

@ -3,11 +3,13 @@ from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from structlog import get_logger
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import Policy from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_" CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_"
CACHE_KEY_USER_PREFIX = "authentik_reputation_user_" CACHE_KEY_USER_PREFIX = "authentik_reputation_user_"
@ -34,9 +36,13 @@ class ReputationPolicy(Policy):
passing = True passing = True
if self.check_ip: if self.check_ip:
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0) score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
LOGGER.debug("Score for IP", ip=remote_ip, score=score)
passing = passing and score <= self.threshold passing = passing and score <= self.threshold
if self.check_username: if self.check_username:
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0) score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
LOGGER.debug(
"Score for Username", username=request.user.username, score=score
)
passing = passing and score <= self.threshold passing = passing and score <= self.threshold
return PolicyResult(passing) return PolicyResult(passing)

View file

@ -1,7 +1,7 @@
"""test reputation signals and policy""" """test reputation signals and policy"""
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import RequestFactory, TestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.policies.reputation.models import ( from authentik.policies.reputation.models import (
@ -19,7 +19,9 @@ class TestReputationPolicy(TestCase):
"""test reputation signals and policy""" """test reputation signals and policy"""
def setUp(self): def setUp(self):
self.test_ip = "255.255.255.255" self.request_factory = RequestFactory()
self.request = self.request_factory.get("/")
self.test_ip = "127.0.0.1"
self.test_username = "test" self.test_username = "test"
cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip) cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
cache.delete(CACHE_KEY_USER_PREFIX + self.test_username) cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
@ -29,7 +31,9 @@ class TestReputationPolicy(TestCase):
def test_ip_reputation(self): def test_ip_reputation(self):
"""test IP reputation""" """test IP reputation"""
# Trigger negative reputation # Trigger negative reputation
authenticate(None, username=self.test_username, password=self.test_username) authenticate(
self.request, username=self.test_username, password=self.test_username
)
# Test value in cache # Test value in cache
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1) self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
# Save cache and check db values # Save cache and check db values
@ -39,7 +43,9 @@ class TestReputationPolicy(TestCase):
def test_user_reputation(self): def test_user_reputation(self):
"""test User reputation""" """test User reputation"""
# Trigger negative reputation # Trigger negative reputation
authenticate(None, username=self.test_username, password=self.test_username) authenticate(
self.request, username=self.test_username, password=self.test_username
)
# Test value in cache # Test value in cache
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1) self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
# Save cache and check db values # Save cache and check db values

View file

@ -0,0 +1,31 @@
# Generated by Django 3.2.4 on 2021-06-14 15:32
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_identification", "0010_identificationstage_password_stage"),
]
operations = [
migrations.AlterField(
model_name="identificationstage",
name="user_fields",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("email", "E Mail"),
("username", "Username"),
("upn", "Upn"),
],
max_length=100,
),
blank=True,
help_text="Fields of the user object to match against. (Hold shift to select multiple options)",
size=None,
),
),
]

View file

@ -9,6 +9,7 @@ from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
DEFAULT_TENANT = Tenant(domain="fallback")
def get_tenant_for_request(request: HttpRequest) -> Tenant: def get_tenant_for_request(request: HttpRequest) -> Tenant:
@ -17,13 +18,13 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant:
Q(domain__iendswith=request.get_host()) | _q_default Q(domain__iendswith=request.get_host()) | _q_default
) )
if not db_tenants.exists(): if not db_tenants.exists():
return Tenant(domain="fallback") return DEFAULT_TENANT
return db_tenants.first() return db_tenants.first()
def context_processor(request: HttpRequest) -> dict[str, Any]: def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template""" """Context Processor that injects tenant object into every template"""
tenant = getattr(request, "tenant", Tenant(domain="fallback")) tenant = getattr(request, "tenant", DEFAULT_TENANT)
return { return {
"tenant": tenant, "tenant": tenant,
"ak_version": __version__, "ak_version": __version__,

View file

@ -3418,6 +3418,11 @@ paths:
description: A search term. description: A search term.
schema: schema:
type: string type: string
- in: query
name: tenant_name
schema:
type: string
description: Tenant name
- in: query - in: query
name: username name: username
schema: schema:
@ -3534,6 +3539,11 @@ paths:
description: A search term. description: A search term.
schema: schema:
type: string type: string
- in: query
name: tenant_name
schema:
type: string
description: Tenant name
- in: query - in: query
name: top_n name: top_n
schema: schema:
@ -19081,6 +19091,9 @@ components:
expires: expires:
type: string type: string
format: date-time format: date-time
tenant:
type: object
additionalProperties: {}
required: required:
- action - action
- app - app
@ -19207,6 +19220,9 @@ components:
expires: expires:
type: string type: string
format: date-time format: date-time
tenant:
type: object
additionalProperties: {}
required: required:
- action - action
- app - app

View file

@ -8,10 +8,22 @@ export interface EventUser {
} }
export interface EventContext { export interface EventContext {
[key: string]: EventContext | string | number | string[]; [key: string]: EventContext | EventModel | string | number | string[];
} }
export interface EventWithContext extends Event { export interface EventWithContext extends Event {
user: EventUser; user: EventUser;
context: EventContext; context: EventContext;
} }
export interface EventModel {
pk: string;
name: string;
app: string;
model_name: string;
}
export interface EventRequest {
path: string;
method: string;
}

View file

@ -3768,6 +3768,7 @@ msgstr "Task finished with warnings"
msgid "Template" msgid "Template"
msgstr "Template" msgstr "Template"
#: src/pages/events/EventListPage.ts
#: src/pages/tenants/TenantListPage.ts #: src/pages/tenants/TenantListPage.ts
msgid "Tenant" msgid "Tenant"
msgstr "Tenant" msgstr "Tenant"

View file

@ -3760,6 +3760,7 @@ msgstr ""
msgid "Template" msgid "Template"
msgstr "" msgstr ""
#:
#: #:
msgid "Tenant" msgid "Tenant"
msgstr "" msgstr ""

View file

@ -5,7 +5,7 @@ import { EventMatcherPolicyActionEnum, FlowsApi } from "authentik-api";
import "../../elements/Spinner"; import "../../elements/Spinner";
import "../../elements/Expand"; import "../../elements/Expand";
import { PFSize } from "../../elements/Spinner"; import { PFSize } from "../../elements/Spinner";
import { EventContext, EventWithContext } from "../../api/Events"; import { EventContext, EventModel, EventWithContext } from "../../api/Events";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG } from "../../api/Config";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@ -41,7 +41,7 @@ export class EventInfo extends LitElement {
]; ];
} }
getModelInfo(context: EventContext): TemplateResult { getModelInfo(context: EventModel): TemplateResult {
if (context === null) { if (context === null) {
return html`<span>-</span>`; return html`<span>-</span>`;
} }
@ -51,7 +51,7 @@ export class EventInfo extends LitElement {
<span class="pf-c-description-list__text">${t`UID`}</span> <span class="pf-c-description-list__text">${t`UID`}</span>
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.pk as string}</div> <div class="pf-c-description-list__text">${context.pk}</div>
</dd> </dd>
</div> </div>
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">
@ -59,7 +59,7 @@ export class EventInfo extends LitElement {
<span class="pf-c-description-list__text">${t`Name`}</span> <span class="pf-c-description-list__text">${t`Name`}</span>
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.name as string}</div> <div class="pf-c-description-list__text">${context.name}</div>
</dd> </dd>
</div> </div>
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">
@ -67,7 +67,7 @@ export class EventInfo extends LitElement {
<span class="pf-c-description-list__text">${t`App`}</span> <span class="pf-c-description-list__text">${t`App`}</span>
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.app as string}</div> <div class="pf-c-description-list__text">${context.app}</div>
</dd> </dd>
</div> </div>
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">
@ -75,7 +75,7 @@ export class EventInfo extends LitElement {
<span class="pf-c-description-list__text">${t`Model Name`}</span> <span class="pf-c-description-list__text">${t`Model Name`}</span>
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.model_name as string}</div> <div class="pf-c-description-list__text">${context.model_name}</div>
</dd> </dd>
</div> </div>
</dl>`; </dl>`;
@ -138,7 +138,12 @@ export class EventInfo extends LitElement {
</div>`; </div>`;
} }
buildGitHubIssueUrl(title: string, body: string): string { buildGitHubIssueUrl(context: EventContext): string {
const httpRequest = this.event.context.http_request as EventContext;
let title = "";
if (httpRequest) {
title = `${httpRequest?.method} ${httpRequest?.path}`;
}
// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters // https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters
const fullBody = ` const fullBody = `
**Describe the bug** **Describe the bug**
@ -162,7 +167,7 @@ If applicable, add screenshots to help explain your problem.
<summary>Stacktrace from authentik</summary> <summary>Stacktrace from authentik</summary>
\`\`\` \`\`\`
${body} ${context.message as string}
\`\`\` \`\`\`
</details> </details>
@ -174,7 +179,9 @@ ${body}
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
`; `;
return `https://github.com/goauthentik/authentik/issues/new?labels=bug+from_authentik&title=${encodeURIComponent(title)}&body=${encodeURIComponent(fullBody)}`; return `https://github.com/goauthentik/authentik/issues/
new?labels=bug+from_authentik&title=${encodeURIComponent(title)}
&body=${encodeURIComponent(fullBody)}`.trim();
} }
render(): TemplateResult { render(): TemplateResult {
@ -187,13 +194,13 @@ Add any other context about the problem here.
case EventMatcherPolicyActionEnum.ModelDeleted: case EventMatcherPolicyActionEnum.ModelDeleted:
return html` return html`
<h3>${t`Affected model:`}</h3> <h3>${t`Affected model:`}</h3>
${this.getModelInfo(this.event.context?.model as EventContext)} ${this.getModelInfo(this.event.context?.model as EventModel)}
`; `;
case EventMatcherPolicyActionEnum.AuthorizeApplication: case EventMatcherPolicyActionEnum.AuthorizeApplication:
return html`<div class="pf-l-flex"> return html`<div class="pf-l-flex">
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Authorized application:`}</h3> <h3>${t`Authorized application:`}</h3>
${this.getModelInfo(this.event.context.authorized_application as EventContext)} ${this.getModelInfo(this.event.context.authorized_application as EventModel)}
</div> </div>
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Using flow`}</h3> <h3>${t`Using flow`}</h3>
@ -215,15 +222,14 @@ Add any other context about the problem here.
case EventMatcherPolicyActionEnum.SecretView: case EventMatcherPolicyActionEnum.SecretView:
return html` return html`
<h3>${t`Secret:`}</h3> <h3>${t`Secret:`}</h3>
${this.getModelInfo(this.event.context.secret as EventContext)}`; ${this.getModelInfo(this.event.context.secret as EventModel)}`;
case EventMatcherPolicyActionEnum.SystemException: case EventMatcherPolicyActionEnum.SystemException:
return html` return html`
<a <a
class="pf-c-button pf-m-primary" class="pf-c-button pf-m-primary"
target="_blank" target="_blank"
href=${this.buildGitHubIssueUrl( href=${this.buildGitHubIssueUrl(
"", this.event.context
this.event.context.message as string
)}> )}>
${t`Open issue on GitHub...`} ${t`Open issue on GitHub...`}
</a> </a>
@ -250,12 +256,12 @@ Add any other context about the problem here.
return html`<div class="pf-l-flex"> return html`<div class="pf-l-flex">
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Binding`}</h3> <h3>${t`Binding`}</h3>
${this.getModelInfo(this.event.context.binding as EventContext)} ${this.getModelInfo(this.event.context.binding as EventModel)}
</div> </div>
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Request`}</h3> <h3>${t`Request`}</h3>
<ul class="pf-c-list"> <ul class="pf-c-list">
<li>${t`Object`}: ${this.getModelInfo((this.event.context.request as EventContext).obj as EventContext)}</li> <li>${t`Object`}: ${this.getModelInfo((this.event.context.request as EventContext).obj as EventModel)}</li>
<li><span>${t`Context`}: <code>${JSON.stringify((this.event.context.request as EventContext).context, null, 4)}</code></span></li> <li><span>${t`Context`}: <code>${JSON.stringify((this.event.context.request as EventContext).context, null, 4)}</code></span></li>
</ul> </ul>
</div> </div>
@ -269,12 +275,12 @@ Add any other context about the problem here.
return html`<div class="pf-l-flex"> return html`<div class="pf-l-flex">
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Binding`}</h3> <h3>${t`Binding`}</h3>
${this.getModelInfo(this.event.context.binding as EventContext)} ${this.getModelInfo(this.event.context.binding as EventModel)}
</div> </div>
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Request`}</h3> <h3>${t`Request`}</h3>
<ul class="pf-c-list"> <ul class="pf-c-list">
<li>${t`Object`}: ${this.getModelInfo((this.event.context.request as EventContext).obj as EventContext)}</li> <li>${t`Object`}: ${this.getModelInfo((this.event.context.request as EventContext).obj as EventModel)}</li>
<li><span>${t`Context`}: <code>${JSON.stringify((this.event.context.request as EventContext).context, null, 4)}</code></span></li> <li><span>${t`Context`}: <code>${JSON.stringify((this.event.context.request as EventContext).context, null, 4)}</code></span></li>
</ul> </ul>
</div> </div>
@ -310,7 +316,7 @@ Add any other context about the problem here.
return html`<div class="pf-l-flex"> return html`<div class="pf-l-flex">
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${t`Using source`}</h3> <h3>${t`Using source`}</h3>
${this.getModelInfo(this.event.context.using_source as EventContext)} ${this.getModelInfo(this.event.context.using_source as EventModel)}
</div> </div>
</div>`; </div>`;
} }

View file

@ -44,6 +44,7 @@ export class EventListPage extends TablePage<Event> {
new TableColumn(t`User`, "user"), new TableColumn(t`User`, "user"),
new TableColumn(t`Creation Date`, "created"), new TableColumn(t`Creation Date`, "created"),
new TableColumn(t`Client IP`, "client_ip"), new TableColumn(t`Client IP`, "client_ip"),
new TableColumn(t`Tenant`, "tenant_name"),
new TableColumn(""), new TableColumn(""),
]; ];
} }
@ -62,6 +63,7 @@ export class EventListPage extends TablePage<Event> {
html`-`, html`-`,
html`<span>${item.created?.toLocaleString()}</span>`, html`<span>${item.created?.toLocaleString()}</span>`,
html`<span>${item.clientIp || "-"}</span>`, html`<span>${item.clientIp || "-"}</span>`,
html`<span>${item.tenant?.name || "-"}</span>`,
html`<a href="#/events/log/${item.pk}"> html`<a href="#/events/log/${item.pk}">
<i class="fas fas fa-share-square"></i> <i class="fas fas fa-share-square"></i>
</a>`, </a>`,