events: add tenant to event
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
e584fd1344
commit
74e578c2bf
|
@ -36,6 +36,7 @@ class EventSerializer(ModelSerializer):
|
|||
"client_ip",
|
||||
"created",
|
||||
"expires",
|
||||
"tenant",
|
||||
]
|
||||
|
||||
|
||||
|
@ -76,6 +77,11 @@ class EventsFilter(django_filters.FilterSet):
|
|||
field_name="action",
|
||||
lookup_expr="icontains",
|
||||
)
|
||||
tenant_name = django_filters.CharFilter(
|
||||
field_name="tenant",
|
||||
lookup_expr="name",
|
||||
label="Tenant name",
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def filter_context_model_pk(self, queryset, name, value):
|
||||
|
|
|
@ -40,9 +40,9 @@ class GeoIPReader:
|
|||
return
|
||||
try:
|
||||
reader = Reader(path)
|
||||
LOGGER.info("Loaded GeoIP database")
|
||||
self.__reader = reader
|
||||
self.__last_mtime = stat(path).st_mtime
|
||||
LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime)
|
||||
except OSError as exc:
|
||||
LOGGER.warning("Failed to load GeoIP database", exc=exc)
|
||||
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
|
@ -21,11 +21,12 @@ from authentik.core.middleware import (
|
|||
)
|
||||
from authentik.core.models import ExpiringModel, Group, User
|
||||
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.utils.http import get_client_ip
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tenants.utils import DEFAULT_TENANT
|
||||
|
||||
LOGGER = get_logger("authentik.events")
|
||||
GAUGE_EVENTS = Gauge(
|
||||
|
@ -40,6 +41,11 @@ def default_event_duration():
|
|||
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):
|
||||
"""Error raised when a notification fails to be delivered"""
|
||||
|
||||
|
@ -95,6 +101,7 @@ class Event(ExpiringModel):
|
|||
context = models.JSONField(default=dict, blank=True)
|
||||
client_ip = models.GenericIPAddressField(null=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
|
||||
expires = models.DateTimeField(default=default_event_duration)
|
||||
|
@ -133,6 +140,13 @@ class Event(ExpiringModel):
|
|||
"""Add data from a Django-HttpRequest, allowing the creation of
|
||||
Events independently 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"):
|
||||
original_user = None
|
||||
if hasattr(request, "session"):
|
||||
|
|
|
@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -3,11 +3,13 @@ from django.core.cache import cache
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.policies.models import Policy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_"
|
||||
CACHE_KEY_USER_PREFIX = "authentik_reputation_user_"
|
||||
|
||||
|
@ -34,9 +36,13 @@ class ReputationPolicy(Policy):
|
|||
passing = True
|
||||
if self.check_ip:
|
||||
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
|
||||
if self.check_username:
|
||||
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
|
||||
return PolicyResult(passing)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""test reputation signals and policy"""
|
||||
from django.contrib.auth import authenticate
|
||||
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.policies.reputation.models import (
|
||||
|
@ -19,7 +19,9 @@ class TestReputationPolicy(TestCase):
|
|||
"""test reputation signals and policy"""
|
||||
|
||||
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"
|
||||
cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
|
||||
cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
|
||||
|
@ -29,7 +31,9 @@ class TestReputationPolicy(TestCase):
|
|||
def test_ip_reputation(self):
|
||||
"""test IP 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
|
||||
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
|
||||
# Save cache and check db values
|
||||
|
@ -39,7 +43,9 @@ class TestReputationPolicy(TestCase):
|
|||
def test_user_reputation(self):
|
||||
"""test User 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
|
||||
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
|
||||
# Save cache and check db values
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -9,6 +9,7 @@ from authentik.lib.config import CONFIG
|
|||
from authentik.tenants.models import Tenant
|
||||
|
||||
_q_default = Q(default=True)
|
||||
DEFAULT_TENANT = Tenant(domain="fallback")
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
if not db_tenants.exists():
|
||||
return Tenant(domain="fallback")
|
||||
return DEFAULT_TENANT
|
||||
return db_tenants.first()
|
||||
|
||||
|
||||
def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||
"""Context Processor that injects tenant object into every template"""
|
||||
tenant = getattr(request, "tenant", Tenant(domain="fallback"))
|
||||
tenant = getattr(request, "tenant", DEFAULT_TENANT)
|
||||
return {
|
||||
"tenant": tenant,
|
||||
"ak_version": __version__,
|
||||
|
|
16
schema.yml
16
schema.yml
|
@ -3418,6 +3418,11 @@ paths:
|
|||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: tenant_name
|
||||
schema:
|
||||
type: string
|
||||
description: Tenant name
|
||||
- in: query
|
||||
name: username
|
||||
schema:
|
||||
|
@ -3534,6 +3539,11 @@ paths:
|
|||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: tenant_name
|
||||
schema:
|
||||
type: string
|
||||
description: Tenant name
|
||||
- in: query
|
||||
name: top_n
|
||||
schema:
|
||||
|
@ -19081,6 +19091,9 @@ components:
|
|||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
tenant:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- action
|
||||
- app
|
||||
|
@ -19207,6 +19220,9 @@ components:
|
|||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
tenant:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- action
|
||||
- app
|
||||
|
|
|
@ -8,10 +8,22 @@ export interface EventUser {
|
|||
}
|
||||
|
||||
export interface EventContext {
|
||||
[key: string]: EventContext | string | number | string[];
|
||||
[key: string]: EventContext | EventModel | string | number | string[];
|
||||
}
|
||||
|
||||
export interface EventWithContext extends Event {
|
||||
user: EventUser;
|
||||
context: EventContext;
|
||||
}
|
||||
|
||||
export interface EventModel {
|
||||
pk: string;
|
||||
name: string;
|
||||
app: string;
|
||||
model_name: string;
|
||||
}
|
||||
|
||||
export interface EventRequest {
|
||||
path: string;
|
||||
method: string;
|
||||
}
|
||||
|
|
|
@ -3768,6 +3768,7 @@ msgstr "Task finished with warnings"
|
|||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
#: src/pages/events/EventListPage.ts
|
||||
#: src/pages/tenants/TenantListPage.ts
|
||||
msgid "Tenant"
|
||||
msgstr "Tenant"
|
||||
|
|
|
@ -3760,6 +3760,7 @@ msgstr ""
|
|||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
#:
|
||||
msgid "Tenant"
|
||||
msgstr ""
|
||||
|
|
|
@ -5,7 +5,7 @@ import { EventMatcherPolicyActionEnum, FlowsApi } from "authentik-api";
|
|||
import "../../elements/Spinner";
|
||||
import "../../elements/Expand";
|
||||
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 PFButton from "@patternfly/patternfly/components/Button/button.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) {
|
||||
return html`<span>-</span>`;
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export class EventInfo extends LitElement {
|
|||
<span class="pf-c-description-list__text">${t`UID`}</span>
|
||||
</dt>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</dt>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</dt>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</dt>
|
||||
<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>
|
||||
</div>
|
||||
</dl>`;
|
||||
|
@ -138,7 +138,12 @@ export class EventInfo extends LitElement {
|
|||
</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
|
||||
const fullBody = `
|
||||
**Describe the bug**
|
||||
|
@ -162,7 +167,7 @@ If applicable, add screenshots to help explain your problem.
|
|||
<summary>Stacktrace from authentik</summary>
|
||||
|
||||
\`\`\`
|
||||
${body}
|
||||
${context.message as string}
|
||||
\`\`\`
|
||||
</details>
|
||||
|
||||
|
@ -174,7 +179,9 @@ ${body}
|
|||
**Additional context**
|
||||
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 {
|
||||
|
@ -187,13 +194,13 @@ Add any other context about the problem here.
|
|||
case EventMatcherPolicyActionEnum.ModelDeleted:
|
||||
return html`
|
||||
<h3>${t`Affected model:`}</h3>
|
||||
${this.getModelInfo(this.event.context?.model as EventContext)}
|
||||
${this.getModelInfo(this.event.context?.model as EventModel)}
|
||||
`;
|
||||
case EventMatcherPolicyActionEnum.AuthorizeApplication:
|
||||
return html`<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<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 class="pf-l-flex__item">
|
||||
<h3>${t`Using flow`}</h3>
|
||||
|
@ -215,15 +222,14 @@ Add any other context about the problem here.
|
|||
case EventMatcherPolicyActionEnum.SecretView:
|
||||
return html`
|
||||
<h3>${t`Secret:`}</h3>
|
||||
${this.getModelInfo(this.event.context.secret as EventContext)}`;
|
||||
${this.getModelInfo(this.event.context.secret as EventModel)}`;
|
||||
case EventMatcherPolicyActionEnum.SystemException:
|
||||
return html`
|
||||
<a
|
||||
class="pf-c-button pf-m-primary"
|
||||
target="_blank"
|
||||
href=${this.buildGitHubIssueUrl(
|
||||
"",
|
||||
this.event.context.message as string
|
||||
this.event.context
|
||||
)}>
|
||||
${t`Open issue on GitHub...`}
|
||||
</a>
|
||||
|
@ -250,12 +256,12 @@ Add any other context about the problem here.
|
|||
return html`<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<h3>${t`Binding`}</h3>
|
||||
${this.getModelInfo(this.event.context.binding as EventContext)}
|
||||
${this.getModelInfo(this.event.context.binding as EventModel)}
|
||||
</div>
|
||||
<div class="pf-l-flex__item">
|
||||
<h3>${t`Request`}</h3>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -269,12 +275,12 @@ Add any other context about the problem here.
|
|||
return html`<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<h3>${t`Binding`}</h3>
|
||||
${this.getModelInfo(this.event.context.binding as EventContext)}
|
||||
${this.getModelInfo(this.event.context.binding as EventModel)}
|
||||
</div>
|
||||
<div class="pf-l-flex__item">
|
||||
<h3>${t`Request`}</h3>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -310,7 +316,7 @@ Add any other context about the problem here.
|
|||
return html`<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<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>`;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ export class EventListPage extends TablePage<Event> {
|
|||
new TableColumn(t`User`, "user"),
|
||||
new TableColumn(t`Creation Date`, "created"),
|
||||
new TableColumn(t`Client IP`, "client_ip"),
|
||||
new TableColumn(t`Tenant`, "tenant_name"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
@ -62,6 +63,7 @@ export class EventListPage extends TablePage<Event> {
|
|||
html`-`,
|
||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||
html`<span>${item.clientIp || "-"}</span>`,
|
||||
html`<span>${item.tenant?.name || "-"}</span>`,
|
||||
html`<a href="#/events/log/${item.pk}">
|
||||
<i class="fas fas fa-share-square"></i>
|
||||
</a>`,
|
||||
|
|
Reference in New Issue