events: rename audit to events and use for more metrics (#397)

* events: rename audit to events

* policies/expression: log expression exceptions as event

* policies/expression: add ExpressionPolicy Model to event when possible

* lib/expressions: ensure syntax errors are logged too

* lib: fix lint error

* policies: add execution_logging field

* core: add property mapping tests

* policies/expression: add full test

* policies/expression: fix attribute name

* policies: add execution_logging

* web: fix imports

* root: update swagger

* policies: use dataclass instead of dict for types

* events: add support for dataclass as event param

* events: add special keys which are never cleaned

* policies: add tests for process, don't clean full cache

* admin: create event when new version is seen

* events: move utils to separate file

* admin: add tests for admin tasks

* events: add .set_user method to ensure users have correct attributes set

* core: add test for property_mapping errors with user and request
This commit is contained in:
Jens L 2020-12-20 22:04:29 +01:00 committed by GitHub
parent 4d88dcff08
commit a4dc6d13b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 907 additions and 383 deletions

View file

@ -17,7 +17,7 @@ from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from authentik.audit.models import Event, EventAction
from authentik.events.models import Event, EventAction
def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:

View file

@ -51,7 +51,7 @@ class VersionViewSet(ListModelMixin, GenericViewSet):
permission_classes = [IsAdminUser]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return None
@swagger_auto_schema(responses={200: VersionSerializer(many=True)})

View file

@ -15,7 +15,7 @@ class WorkerViewSet(ListModelMixin, GenericViewSet):
serializer_class = Serializer
permission_classes = [IsAdminUser]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return None
def list(self, request: Request) -> Response:

View file

@ -1,8 +1,11 @@
"""authentik admin tasks"""
from django.core.cache import cache
from packaging.version import parse
from requests import RequestException, get
from structlog import get_logger
from authentik import __version__
from authentik.events.models import Event, EventAction
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP
@ -19,12 +22,24 @@ def update_latest_version(self: MonitoredTask):
response.raise_for_status()
data = response.json()
tag_name = data.get("tag_name")
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT)
upstream_version = tag_name.split("/")[1]
cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
self.set_status(
TaskResult(
TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]
)
)
# Check if upstream version is newer than what we're running,
# and if no event exists yet, create one.
local_version = parse(__version__)
if local_version < parse(upstream_version):
# Event has already been created, don't create duplicate
if Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE,
context__new_version=upstream_version,
).exists():
return
Event.new(EventAction.UPDATE_AVAILABLE, new_version=upstream_version).save()
except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))

View file

@ -0,0 +1,76 @@
"""test admin tasks"""
import json
from dataclasses import dataclass
from unittest.mock import Mock, patch
from django.core.cache import cache
from django.test import TestCase
from requests.exceptions import RequestException
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.events.models import Event, EventAction
@dataclass
class MockResponse:
"""Mock class to emulate the methods of requests's Response we need"""
status_code: int
response: str
def json(self) -> dict:
"""Get json parsed response"""
return json.loads(self.response)
def raise_for_status(self):
"""raise RequestException if status code is 400 or more"""
if self.status_code >= 400:
raise RequestException
REQUEST_MOCK_VALID = Mock(
return_value=MockResponse(
200,
"""{
"tag_name": "version/1.2.3"
}""",
)
)
REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}"))
class TestAdminTasks(TestCase):
"""test admin tasks"""
@patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID)
def test_version_valid_response(self):
"""Test Update checker with valid response"""
update_latest_version.delay().get()
self.assertEqual(cache.get(VERSION_CACHE_KEY), "1.2.3")
self.assertTrue(
Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
).exists()
)
# test that a consecutive check doesn't create a duplicate event
update_latest_version.delay().get()
self.assertEqual(
len(
Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
)
),
1,
)
@patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID)
def test_version_error(self):
"""Test Update checker with invalid response"""
update_latest_version.delay().get()
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
self.assertFalse(
Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
).exists()
)

View file

@ -11,7 +11,6 @@ from authentik.admin.api.version import VersionViewSet
from authentik.admin.api.workers import WorkerViewSet
from authentik.api.v2.config import ConfigsViewSet
from authentik.api.v2.messages import MessagesViewSet
from authentik.audit.api import EventViewSet
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
@ -20,6 +19,7 @@ from authentik.core.api.sources import SourceViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet
from authentik.crypto.api import CertificateKeyPairViewSet
from authentik.events.api import EventViewSet
from authentik.flows.api import (
FlowCacheViewSet,
FlowStageBindingViewSet,
@ -96,7 +96,7 @@ router.register("flows/bindings", FlowStageBindingViewSet)
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
router.register("audit/events", EventViewSet)
router.register("events/events", EventViewSet)
router.register("sources/all", SourceViewSet)
router.register("sources/ldap", LDAPSourceViewSet)

View file

@ -1,16 +0,0 @@
"""authentik audit app"""
from importlib import import_module
from django.apps import AppConfig
class AuthentikAuditConfig(AppConfig):
"""authentik audit app"""
name = "authentik.audit"
label = "authentik_audit"
verbose_name = "authentik Audit"
mountpoint = "audit/"
def ready(self):
import_module("authentik.audit.signals")

View file

@ -1,9 +0,0 @@
"""authentik audit urls"""
from django.urls import path
from authentik.audit.views import EventListView
urlpatterns = [
# Audit Log
path("audit/", EventListView.as_view(), name="log"),
]

View file

@ -12,8 +12,8 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.metrics import get_events_per_1h
from authentik.audit.models import EventAction
from authentik.core.models import Application
from authentik.events.models import EventAction
from authentik.policies.engine import PolicyEngine
@ -78,7 +78,7 @@ class ApplicationViewSet(ModelViewSet):
get_objects_for_user(request.user, "authentik_core.view_application"),
slug=slug,
)
if not request.user.has_perm("authentik_audit.view_event"):
if not request.user.has_perm("authentik_events.view_event"):
raise Http404
return Response(
get_events_per_1h(

View file

@ -6,8 +6,8 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.audit.models import Event, EventAction
from authentik.core.models import Token
from authentik.events.models import Event, EventAction
class TokenSerializer(ModelSerializer):

View file

@ -1,9 +1,11 @@
"""Property Mapping Evaluator"""
from traceback import format_tb
from typing import Optional
from django.http import HttpRequest
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.expression.evaluator import BaseEvaluator
@ -19,3 +21,18 @@ class PropertyMappingEvaluator(BaseEvaluator):
if request:
self._context["request"] = request
self._context.update(**kwargs)
def handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler"""
error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)])
event = Event.new(
EventAction.PROPERTY_MAPPING_EXCEPTION,
expression=expression_source,
error=error_string,
)
if "user" in self._context:
event.set_user(self._context["user"])
if "request" in self._context:
event.from_http(self._context["request"])
return
event.save()

View file

@ -0,0 +1,56 @@
"""authentik core property mapping tests"""
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import PropertyMapping
from authentik.events.models import Event, EventAction
class TestPropertyMappings(TestCase):
"""authentik core property mapping tests"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
def test_expression(self):
"""Test expression"""
mapping = PropertyMapping.objects.create(
name="test", expression="return 'test'"
)
self.assertEqual(mapping.evaluate(None, None), "test")
def test_expression_syntax(self):
"""Test expression syntax error"""
mapping = PropertyMapping.objects.create(name="test", expression="-")
with self.assertRaises(PropertyMappingExpressionException):
mapping.evaluate(None, None)
def test_expression_error_general(self):
"""Test expression error"""
expr = "return aaa"
mapping = PropertyMapping.objects.create(name="test", expression=expr)
with self.assertRaises(NameError):
mapping.evaluate(None, None)
events = Event.objects.filter(
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
)
self.assertTrue(events.exists())
self.assertEqual(len(events), 1)
def test_expression_error_extended(self):
"""Test expression error (with user and http request"""
expr = "return aaa"
request = self.factory.get("/")
mapping = PropertyMapping.objects.create(name="test", expression=expr)
with self.assertRaises(NameError):
mapping.evaluate(get_anonymous_user(), request)
events = Event.objects.filter(
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
)
self.assertTrue(events.exists())
self.assertEqual(len(events), 1)
event = events.first()
self.assertEqual(event.user["username"], "AnonymousUser")
self.assertEqual(event.client_ip, "127.0.0.1")

View file

@ -5,12 +5,12 @@ from django.shortcuts import get_object_or_404, redirect
from django.views import View
from structlog import get_logger
from authentik.audit.models import Event, EventAction
from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER,
SESSION_IMPERSONATE_USER,
)
from authentik.core.models import User
from authentik.events.models import Event, EventAction
LOGGER = get_logger()

View file

@ -1,4 +1,4 @@
"""Audit API Views"""
"""Events API Views"""
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform
from drf_yasg2.utils import swagger_auto_schema
@ -9,7 +9,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.audit.models import Event, EventAction
from authentik.events.models import Event, EventAction
class EventSerializer(ModelSerializer):

16
authentik/events/apps.py Normal file
View file

@ -0,0 +1,16 @@
"""authentik events app"""
from importlib import import_module
from django.apps import AppConfig
class AuthentikEventsConfig(AppConfig):
"""authentik events app"""
name = "authentik.events"
label = "authentik_events"
verbose_name = "authentik Events"
mountpoint = "events/"
def ready(self):
import_module("authentik.events.signals")

View file

@ -1,4 +1,4 @@
"""Audit middleware"""
"""Events middleware"""
from functools import partial
from typing import Callable
@ -7,9 +7,10 @@ from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse
from authentik.audit.models import Event, EventAction, model_to_dict
from authentik.audit.signals import EventNewThread
from authentik.core.middleware import LOCAL
from authentik.events.models import Event, EventAction
from authentik.events.signals import EventNewThread
from authentik.events.utils import model_to_dict
class AuditMiddleware:

View file

@ -63,8 +63,8 @@ class Migration(migrations.Migration):
),
],
options={
"verbose_name": "Audit Event",
"verbose_name_plural": "Audit Events",
"verbose_name": "Event",
"verbose_name_plural": "Events",
},
),
]

View file

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_audit", "0001_initial"),
("authentik_events", "0001_initial"),
]
operations = [

View file

@ -3,11 +3,11 @@ from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.audit.models
import authentik.events.models
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Event = apps.get_model("authentik_audit", "Event")
Event = apps.get_model("authentik_events", "Event")
db_alias = schema_editor.connection.alias
for event in Event.objects.all():
@ -15,7 +15,7 @@ def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
# Because event objects cannot be updated, we have to re-create them
event.pk = None
event.user_json = (
authentik.audit.models.get_user(event.user) if event.user else {}
authentik.events.models.get_user(event.user) if event.user else {}
)
event._state.adding = True
event.save()
@ -24,7 +24,7 @@ def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
class Migration(migrations.Migration):
dependencies = [
("authentik_audit", "0002_auto_20200918_2116"),
("authentik_events", "0002_auto_20200918_2116"),
]
operations = [

View file

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_audit", "0003_auto_20200917_1155"),
("authentik_events", "0003_auto_20200917_1155"),
]
operations = [

View file

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_audit", "0004_auto_20200921_1829"),
("authentik_events", "0004_auto_20200921_1829"),
]
operations = [

View file

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_audit", "0005_auto_20201005_2139"),
("authentik_events", "0005_auto_20201005_2139"),
]
operations = [

View file

@ -0,0 +1,41 @@
# Generated by Django 3.1.4 on 2020-12-15 09:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0006_auto_20201017_2024"),
]
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"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("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"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
]

View file

@ -0,0 +1,42 @@
# Generated by Django 3.1.4 on 2020-12-20 16:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0007_auto_20201215_0939"),
]
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"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("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"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View file

@ -1,17 +1,14 @@
"""authentik audit models"""
"""authentik events models"""
from inspect import getmodule, stack
from typing import Any, Dict, Optional, Union
from uuid import UUID, uuid4
from typing import Optional, Union
from uuid import uuid4
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.base import Model
from django.http import HttpRequest
from django.utils.translation import gettext as _
from django.views.debug import SafeExceptionReporterFilter
from guardian.utils import get_anonymous_user
from structlog import get_logger
from authentik.core.middleware import (
@ -19,78 +16,14 @@ from authentik.core.middleware import (
SESSION_IMPERSONATE_USER,
)
from authentik.core.models import User
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
from authentik.lib.utils.http import get_client_ip
LOGGER = get_logger("authentik.audit")
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""Cleanse a dictionary, recursively"""
final_dict = {}
for key, value in source.items():
try:
if SafeExceptionReporterFilter.hidden_settings.search(key):
final_dict[key] = SafeExceptionReporterFilter.cleansed_substitute
else:
final_dict[key] = value
except TypeError:
final_dict[key] = value
if isinstance(value, dict):
final_dict[key] = cleanse_dict(value)
return final_dict
def model_to_dict(model: Model) -> Dict[str, Any]:
"""Convert model to dict"""
name = str(model)
if hasattr(model, "name"):
name = model.name
return {
"app": model._meta.app_label,
"model_name": model._meta.model_name,
"pk": model.pk,
"name": name,
}
def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]:
"""Convert user object to dictionary, optionally including the original user"""
if isinstance(user, AnonymousUser):
user = get_anonymous_user()
user_data = {
"username": user.username,
"pk": user.pk,
"email": user.email,
}
if original_user:
original_data = get_user(original_user)
original_data["on_behalf_of"] = user_data
return original_data
return user_data
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""clean source of all Models that would interfere with the JSONField.
Models are replaced with a dictionary of {
app: str,
name: str,
pk: Any
}"""
final_dict = {}
for key, value in source.items():
if isinstance(value, dict):
final_dict[key] = sanitize_dict(value)
elif isinstance(value, models.Model):
final_dict[key] = sanitize_dict(model_to_dict(value))
elif isinstance(value, UUID):
final_dict[key] = value.hex
else:
final_dict[key] = value
return final_dict
LOGGER = get_logger("authentik.events")
class EventAction(models.TextChoices):
"""All possible actions to save into the audit log"""
"""All possible actions to save into the events log"""
LOGIN = "login"
LOGIN_FAILED = "login_failed"
@ -111,15 +44,21 @@ class EventAction(models.TextChoices):
IMPERSONATION_STARTED = "impersonation_started"
IMPERSONATION_ENDED = "impersonation_ended"
POLICY_EXECUTION = "policy_execution"
POLICY_EXCEPTION = "policy_exception"
PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
MODEL_CREATED = "model_created"
MODEL_UPDATED = "model_updated"
MODEL_DELETED = "model_deleted"
UPDATE_AVAILABLE = "update_available"
CUSTOM_PREFIX = "custom_"
class Event(models.Model):
"""An individual audit log event"""
"""An individual Audit/Metrics/Notification/Error Event"""
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
user = models.JSONField(default=dict)
@ -151,6 +90,12 @@ class Event(models.Model):
event = Event(action=action, app=app, context=cleaned_kwargs)
return event
def set_user(self, user: User) -> "Event":
"""Set `.user` based on user, ensuring the correct attributes are copied.
This should only be used when self.from_http is *not* used."""
self.user = get_user(user)
return self
def from_http(
self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
) -> "Event":
@ -185,7 +130,7 @@ class Event(models.Model):
"you may not edit an existing %s" % self._meta.model_name
)
LOGGER.debug(
"Created Audit event",
"Created Event",
action=self.action,
context=self.context,
client_ip=self.client_ip,
@ -195,5 +140,5 @@ class Event(models.Model):
class Meta:
verbose_name = _("Audit Event")
verbose_name_plural = _("Audit Events")
verbose_name = _("Event")
verbose_name_plural = _("Events")

View file

@ -1,4 +1,4 @@
"""authentik audit signal listener"""
"""authentik events signal listener"""
from threading import Thread
from typing import Any, Dict, Optional
@ -10,9 +10,9 @@ from django.contrib.auth.signals import (
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.audit.models import Event, EventAction
from authentik.core.models import User
from authentik.core.signals import password_changed
from authentik.events.models import Event, EventAction
from authentik.stages.invitation.models import Invitation
from authentik.stages.invitation.signals import invitation_created, invitation_used
from authentik.stages.user_write.signals import user_write

View file

@ -9,7 +9,7 @@
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-catalog"></i>
{% trans 'Audit Log' %}
{% trans 'Event Log' %}
</h1>
</div>
</section>

View file

@ -1,15 +1,15 @@
"""audit event tests"""
"""events event tests"""
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.audit.models import Event
from authentik.events.models import Event
from authentik.policies.dummy.models import DummyPolicy
class TestAuditEvent(TestCase):
"""Test Audit Event"""
class TestEvents(TestCase):
"""Test Event"""
def test_new_with_model(self):
"""Create a new Event passing a model as kwarg"""

9
authentik/events/urls.py Normal file
View file

@ -0,0 +1,9 @@
"""authentik events urls"""
from django.urls import path
from authentik.events.views import EventListView
urlpatterns = [
# Event Log
path("log/", EventListView.as_view(), name="log"),
]

86
authentik/events/utils.py Normal file
View file

@ -0,0 +1,86 @@
"""event utilities"""
import re
from dataclasses import asdict, is_dataclass
from typing import Any, Dict, Optional
from uuid import UUID
from django.contrib.auth.models import AnonymousUser
from django.db import models
from django.db.models.base import Model
from django.views.debug import SafeExceptionReporterFilter
from guardian.utils import get_anonymous_user
from authentik.core.models import User
# Special keys which are *not* cleaned, even when the default filter
# is matched
ALLOWED_SPECIAL_KEYS = re.compile("passing", flags=re.I)
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""Cleanse a dictionary, recursively"""
final_dict = {}
for key, value in source.items():
try:
if SafeExceptionReporterFilter.hidden_settings.search(
key
) and not ALLOWED_SPECIAL_KEYS.search(key):
final_dict[key] = SafeExceptionReporterFilter.cleansed_substitute
else:
final_dict[key] = value
except TypeError:
final_dict[key] = value
if isinstance(value, dict):
final_dict[key] = cleanse_dict(value)
return final_dict
def model_to_dict(model: Model) -> Dict[str, Any]:
"""Convert model to dict"""
name = str(model)
if hasattr(model, "name"):
name = model.name
return {
"app": model._meta.app_label,
"model_name": model._meta.model_name,
"pk": model.pk,
"name": name,
}
def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]:
"""Convert user object to dictionary, optionally including the original user"""
if isinstance(user, AnonymousUser):
user = get_anonymous_user()
user_data = {
"username": user.username,
"pk": user.pk,
"email": user.email,
}
if original_user:
original_data = get_user(original_user)
original_data["on_behalf_of"] = user_data
return original_data
return user_data
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""clean source of all Models that would interfere with the JSONField.
Models are replaced with a dictionary of {
app: str,
name: str,
pk: Any
}"""
final_dict = {}
for key, value in source.items():
if is_dataclass(value):
value = asdict(value)
if isinstance(value, dict):
final_dict[key] = sanitize_dict(value)
elif isinstance(value, models.Model):
final_dict[key] = sanitize_dict(model_to_dict(value))
elif isinstance(value, UUID):
final_dict[key] = value.hex
else:
final_dict[key] = value
return final_dict

View file

@ -4,7 +4,7 @@ from django.views.generic import ListView
from guardian.mixins import PermissionListMixin
from authentik.admin.views.utils import SearchListMixin, UserPaginateListMixin
from authentik.audit.models import Event
from authentik.events.models import Event
class EventListView(
@ -17,8 +17,8 @@ class EventListView(
"""Show list of all invitations"""
model = Event
template_name = "audit/list.html"
permission_required = "authentik_audit.view_event"
template_name = "events/list.html"
permission_required = "authentik_events.view_event"
ordering = "-created"
search_fields = [

View file

@ -8,8 +8,8 @@ from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog import get_logger
from authentik.audit.models import cleanse_dict
from authentik.core.models import User
from authentik.events.models import cleanse_dict
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowStageBinding, Stage

View file

@ -17,8 +17,8 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import TemplateView, View
from structlog import get_logger
from authentik.audit.models import cleanse_dict
from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import cleanse_dict
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
from authentik.flows.planner import (

View file

@ -80,11 +80,15 @@ class BaseEvaluator:
span: Span
span.set_data("expression", expression_source)
param_keys = self._context.keys()
try:
ast_obj = compile(
self.wrap_expression(expression_source, param_keys),
self._filename,
"exec",
)
except (SyntaxError, ValueError) as exc:
self.handle_error(exc, expression_source)
raise exc
try:
_locals = self._context
# Yes this is an exec, yes it is potentially bad. Since we limit what variables are
@ -94,10 +98,15 @@ class BaseEvaluator:
exec(ast_obj, self._globals, _locals) # nosec # noqa
result = _locals["result"]
except Exception as exc:
LOGGER.warning("Expression error", exc=exc)
raise
self.handle_error(exc, expression_source)
raise exc
return result
# pylint: disable=unused-argument
def handle_error(self, exc: Exception, expression_source: str): # pragma: no cover
"""Exception Handler"""
LOGGER.warning("Expression error", exc=exc)
def validate(self, expression: str) -> bool:
"""Validate expression's syntax, raise ValidationError if Syntax is invalid"""
param_keys = self._context.keys()

View file

@ -1,16 +1,21 @@
"""authentik expression policy evaluator"""
from ipaddress import ip_address, ip_network
from typing import List
from traceback import format_tb
from typing import TYPE_CHECKING, List, Optional
from django.http import HttpRequest
from structlog import get_logger
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict, sanitize_dict
from authentik.flows.planner import PLAN_CONTEXT_SSO
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.http import get_client_ip
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
if TYPE_CHECKING:
from authentik.policies.expression.models import ExpressionPolicy
class PolicyEvaluator(BaseEvaluator):
@ -18,6 +23,8 @@ class PolicyEvaluator(BaseEvaluator):
_messages: List[str]
policy: Optional["ExpressionPolicy"] = None
def __init__(self, policy_name: str):
super().__init__()
self._messages = []
@ -45,15 +52,30 @@ class PolicyEvaluator(BaseEvaluator):
self._context["ak_client_ip"] = ip_address(
get_client_ip(request) or "255.255.255.255"
)
self._context["request"] = request
self._context["http_request"] = request
def handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler"""
error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)])
event = Event.new(
EventAction.POLICY_EXCEPTION,
expression=expression_source,
error=error_string,
request=self._context["request"],
)
if self.policy:
event.context["model"] = sanitize_dict(model_to_dict(self.policy))
if "http_request" in self._context:
event.from_http(self._context["http_request"])
else:
event.set_user(self._context["request"].user)
event.save()
def evaluate(self, expression_source: str) -> PolicyResult:
"""Parse and evaluate expression. Policy is expected to return a truthy object.
Messages can be added using 'do ak_message()'."""
try:
result = super().evaluate(expression_source)
except (ValueError, SyntaxError) as exc:
return PolicyResult(False, str(exc))
except Exception as exc: # pylint: disable=broad-except
LOGGER.warning("Expression error", exc=exc)
return PolicyResult(False, str(exc))

View file

@ -31,11 +31,14 @@ class ExpressionPolicy(Policy):
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
evaluator = PolicyEvaluator(self.name)
evaluator.policy = self
evaluator.set_policy_request(request)
return evaluator.evaluate(self.expression)
def save(self, *args, **kwargs):
PolicyEvaluator(self.name).validate(self.expression)
evaluator = PolicyEvaluator(self.name)
evaluator.policy = self
evaluator.validate(self.expression)
return super().save(*args, **kwargs)
class Meta:

View file

@ -3,7 +3,9 @@ from django.core.exceptions import ValidationError
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.events.models import Event, EventAction
from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.types import PolicyRequest
@ -13,6 +15,14 @@ class TestEvaluator(TestCase):
def setUp(self):
self.request = PolicyRequest(user=get_anonymous_user())
def test_full(self):
"""Test full with Policy instance"""
policy = ExpressionPolicy(name="test", expression="return 'test'")
policy.save()
request = PolicyRequest(get_anonymous_user())
result = policy.passes(request)
self.assertTrue(result.passing)
def test_valid(self):
"""test simple value expression"""
template = "return True"
@ -37,6 +47,12 @@ class TestEvaluator(TestCase):
result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
self.assertTrue(
Event.objects.filter(
action=EventAction.POLICY_EXCEPTION,
context__expression=template,
).exists()
)
def test_undefined(self):
"""test undefined result"""
@ -46,6 +62,12 @@ class TestEvaluator(TestCase):
result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("name 'foo' is not defined",))
self.assertTrue(
Event.objects.filter(
action=EventAction.POLICY_EXCEPTION,
context__expression=template,
).exists()
)
def test_validate(self):
"""test validate"""

View file

@ -5,7 +5,7 @@ from django import forms
from authentik.lib.widgets import GroupedModelChoiceField
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
GENERAL_FIELDS = ["name"]
GENERAL_FIELDS = ["name", "execution_logging"]
GENERAL_SERIALIZER_FIELDS = ["pk", "name"]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.1.4 on 2020-12-15 09:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies", "0003_auto_20200908_1542"),
]
operations = [
migrations.AddField(
model_name="policy",
name="execution_logging",
field=models.BooleanField(
default=False,
help_text="When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.",
),
),
]

View file

@ -81,6 +81,16 @@ class Policy(SerializerModel, CreatedUpdatedModel):
name = models.TextField(blank=True, null=True)
execution_logging = models.BooleanField(
default=False,
help_text=_(
(
"When this option is enabled, all executions of this policy will be logged. "
"By default, only execution errors are logged."
)
),
)
objects = InheritanceAutoManager()
@property

View file

@ -8,6 +8,7 @@ from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog import get_logger
from authentik.events.models import Event, EventAction
from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding
from authentik.policies.types import PolicyRequest, PolicyResult
@ -48,12 +49,6 @@ class PolicyProcess(Process):
def execute(self) -> PolicyResult:
"""Run actual policy, returns result"""
with Hub.current.start_span(
op="policy.process.execute",
) as span:
span: Span
span.set_data("policy", self.binding.policy)
span.set_data("request", self.request)
LOGGER.debug(
"P_ENG(proc): Running policy",
policy=self.binding.policy,
@ -62,6 +57,14 @@ class PolicyProcess(Process):
)
try:
policy_result = self.binding.policy.passes(self.request)
if self.binding.policy.execution_logging:
event = Event.new(
EventAction.POLICY_EXECUTION,
request=self.request,
result=policy_result,
)
event.set_user(self.request.user)
event.save()
except PolicyException as exc:
LOGGER.debug("P_ENG(proc): error", exc=exc)
policy_result = PolicyResult(False, str(exc))
@ -82,6 +85,12 @@ class PolicyProcess(Process):
LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
return policy_result
def run(self):
def run(self): # pragma: no cover
"""Task wrapper to run policy checking"""
with Hub.current.start_span(
op="policy.process.execute",
) as span:
span: Span
span.set_data("policy", self.binding.policy)
span.set_data("request", self.request)
self.connection.send(self.execute())

View file

@ -7,13 +7,14 @@ from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.engine import PolicyEngine
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
from authentik.policies.tests.test_process import clear_policy_cache
class TestPolicyEngine(TestCase):
"""PolicyEngine tests"""
def setUp(self):
cache.clear()
clear_policy_cache()
self.user = User.objects.create_user(username="policyuser")
self.policy_false = DummyPolicy.objects.create(
result=False, wait_min=0, wait_max=1
@ -84,10 +85,18 @@ class TestPolicyEngine(TestCase):
def test_engine_cache(self):
"""Ensure empty policy list passes"""
pbm = PolicyBindingModel.objects.create()
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
binding = PolicyBinding.objects.create(
target=pbm, policy=self.policy_false, order=0
)
engine = PolicyEngine(pbm, self.user)
self.assertEqual(len(cache.keys("policy_*")), 0)
self.assertEqual(
len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 0
)
self.assertEqual(engine.build().passing, False)
self.assertEqual(len(cache.keys("policy_*")), 1)
self.assertEqual(
len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1
)
self.assertEqual(engine.build().passing, False)
self.assertEqual(len(cache.keys("policy_*")), 1)
self.assertEqual(
len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1
)

View file

@ -0,0 +1,105 @@
"""policy process tests"""
from django.core.cache import cache
from django.test import TestCase
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest
def clear_policy_cache():
"""Ensure no policy-related keys are stil cached"""
keys = cache.keys("policy_*")
cache.delete(keys)
class TestPolicyProcess(TestCase):
"""Policy Process tests"""
def setUp(self):
clear_policy_cache()
self.user = User.objects.create_user(username="policyuser")
def test_invalid(self):
"""Test Process with invalid arguments"""
policy = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1)
binding = PolicyBinding(policy=policy)
with self.assertRaises(ValueError):
PolicyProcess(binding, None, None) # type: ignore
def test_true(self):
"""Test policy execution"""
policy = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1)
binding = PolicyBinding(policy=policy)
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
self.assertEqual(response.passing, True)
self.assertEqual(response.messages, ("dummy",))
def test_false(self):
"""Test policy execution"""
policy = DummyPolicy.objects.create(result=False, wait_min=0, wait_max=1)
binding = PolicyBinding(policy=policy)
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
self.assertEqual(response.passing, False)
self.assertEqual(response.messages, ("dummy",))
def test_negate(self):
"""Test policy execution"""
policy = DummyPolicy.objects.create(result=False, wait_min=0, wait_max=1)
binding = PolicyBinding(policy=policy, negate=True)
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
self.assertEqual(response.passing, True)
self.assertEqual(response.messages, ("dummy",))
def test_exception(self):
"""Test policy execution"""
policy = Policy.objects.create()
binding = PolicyBinding(policy=policy)
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
self.assertEqual(response.passing, False)
def test_execution_logging(self):
"""Test policy execution creates event"""
policy = DummyPolicy.objects.create(
result=False, wait_min=0, wait_max=1, execution_logging=True
)
binding = PolicyBinding(policy=policy)
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
self.assertEqual(response.passing, False)
self.assertEqual(response.messages, ("dummy",))
events = Event.objects.filter(
action=EventAction.POLICY_EXECUTION,
)
self.assertTrue(events.exists())
self.assertEqual(len(events), 1)
event = events.first()
self.assertEqual(event.context["result"]["passing"], False)
self.assertEqual(event.context["result"]["messages"], ["dummy"])
def test_raises(self):
"""Test policy that raises error"""
policy_raises = ExpressionPolicy.objects.create(
name="raises", expression="{{ 0/0 }}"
)
binding = PolicyBinding(policy=policy_raises)
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
self.assertEqual(response.passing, False)
self.assertEqual(response.messages, ("division by zero",))
# self.assert

View file

@ -1,6 +1,7 @@
"""policy structures"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
from django.db.models import Model
@ -11,6 +12,7 @@ if TYPE_CHECKING:
from authentik.policies.models import Policy
@dataclass
class PolicyRequest:
"""Data-class to hold policy request data"""
@ -20,6 +22,7 @@ class PolicyRequest:
context: Dict[str, str]
def __init__(self, user: User):
super().__init__()
self.user = user
self.http_request = None
self.obj = None
@ -29,6 +32,7 @@ class PolicyRequest:
return f"<PolicyRequest user={self.user}>"
@dataclass
class PolicyResult:
"""Small data-class to hold policy results"""
@ -39,6 +43,7 @@ class PolicyResult:
source_results: Optional[List["PolicyResult"]]
def __init__(self, passing: bool, *messages: str):
super().__init__()
self.passing = passing
self.messages = messages
self.source_policy = None
@ -49,5 +54,5 @@ class PolicyResult:
def __str__(self):
if self.messages:
return f"PolicyResult passing={self.passing} messages={self.messages}"
return f"PolicyResult passing={self.passing}"
return f"<PolicyResult passing={self.passing} messages={self.messages}>"
return f"<PolicyResult passing={self.passing}>"

View file

@ -9,8 +9,8 @@ from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from structlog import get_logger
from authentik.audit.models import Event, EventAction
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_APPLICATION,

View file

@ -11,8 +11,8 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from structlog import get_logger
from authentik.audit.models import Event, EventAction
from authentik.core.models import Application, Provider
from authentik.events.models import Event, EventAction
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_APPLICATION,

View file

@ -86,7 +86,7 @@ INSTALLED_APPS = [
"django.contrib.humanize",
"authentik.admin.apps.AuthentikAdminConfig",
"authentik.api.apps.AuthentikAPIConfig",
"authentik.audit.apps.AuthentikAuditConfig",
"authentik.events.apps.AuthentikEventsConfig",
"authentik.crypto.apps.AuthentikCryptoConfig",
"authentik.flows.apps.AuthentikFlowsConfig",
"authentik.outposts.apps.AuthentikOutpostConfig",
@ -180,7 +180,7 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"authentik.core.middleware.RequestIDMiddleware",
"authentik.audit.middleware.AuditMiddleware",
"authentik.events.middleware.AuditMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",

View file

@ -10,8 +10,8 @@ from django.utils.translation import gettext as _
from django.views.generic import View
from structlog import get_logger
from authentik.audit.models import Event, EventAction
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,

View file

@ -1,8 +1,8 @@
"""OAuth Stages"""
from django.http import HttpRequest, HttpResponse
from authentik.audit.models import Event, EventAction
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.sources.oauth.models import UserOAuthSourceConnection

View file

@ -7,7 +7,7 @@ from django.views import View
from django.views.generic import TemplateView
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from authentik.audit.models import Event
from authentik.events.models import Event
from authentik.stages.otp_static.models import OTPStaticStage

View file

@ -7,7 +7,7 @@ from django.views import View
from django.views.generic import TemplateView
from django_otp.plugins.otp_totp.models import TOTPDevice
from authentik.audit.models import Event
from authentik.events.models import Event
from authentik.stages.otp_time.models import OTPTimeStage

View file

@ -0,0 +1,21 @@
# flake8: noqa
from lifecycle.migrate import BaseMigration
SQL_STATEMENT = """BEGIN TRANSACTION;
ALTER TABLE authentik_audit_event RENAME TO authentik_events_event;
UPDATE django_migrations SET app = replace(app, 'authentik_audit', 'authentik_events');
UPDATE django_content_type SET app_label = replace(app_label, 'authentik_audit', 'authentik_events');
END TRANSACTION;"""
class Migration(BaseMigration):
def needs_migration(self) -> bool:
self.cur.execute(
"select * from information_schema.tables where table_name = 'authentik_audit_event';"
)
return bool(self.cur.rowcount)
def run(self):
self.cur.execute(SQL_STATEMENT)
self.con.commit()

View file

@ -155,112 +155,6 @@ paths:
tags:
- admin
parameters: []
/audit/events/:
get:
operationId: audit_events_list
description: Event Read-Only Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/Event'
tags:
- audit
parameters: []
/audit/events/top_per_user/:
get:
operationId: audit_events_top_per_user
description: Get the top_n events grouped by user count
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: Response object of Event's top_per_user
schema:
description: ''
type: array
items:
$ref: '#/definitions/EventTopPerUserSerialier'
tags:
- audit
parameters: []
/audit/events/{event_uuid}/:
get:
operationId: audit_events_read
description: Event Read-Only Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Event'
tags:
- audit
parameters:
- name: event_uuid
in: path
description: A UUID string identifying this Audit Event.
required: true
type: string
format: uuid
/core/applications/:
get:
operationId: core_applications_list
@ -968,6 +862,112 @@ paths:
required: true
type: string
format: uuid
/events/events/:
get:
operationId: events_events_list
description: Event Read-Only Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/Event'
tags:
- events
parameters: []
/events/events/top_per_user/:
get:
operationId: events_events_top_per_user
description: Get the top_n events grouped by user count
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: Response object of Event's top_per_user
schema:
description: ''
type: array
items:
$ref: '#/definitions/EventTopPerUserSerialier'
tags:
- events
parameters: []
/events/events/{event_uuid}/:
get:
operationId: events_events_read
description: Event Read-Only Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Event'
tags:
- events
parameters:
- name: event_uuid
in: path
description: A UUID string identifying this Event.
required: true
type: string
format: uuid
/flows/bindings/:
get:
operationId: flows_bindings_list
@ -6729,78 +6729,6 @@ definitions:
title: Outdated
type: boolean
readOnly: true
Event:
description: Event Serializer
required:
- action
- app
type: object
properties:
pk:
title: Event uuid
type: string
format: uuid
readOnly: true
user:
title: User
type: object
action:
title: Action
type: string
enum:
- login
- login_failed
- logout
- user_write
- suspicious_request
- password_set
- token_view
- invitation_created
- invitation_used
- authorize_application
- source_linked
- impersonation_started
- impersonation_ended
- model_created
- model_updated
- model_deleted
- custom_
app:
title: App
type: string
minLength: 1
context:
title: Context
type: object
client_ip:
title: Client ip
type: string
minLength: 1
x-nullable: true
created:
title: Created
type: string
format: date-time
readOnly: true
EventTopPerUserSerialier:
description: Response object of Event's top_per_user
required:
- application
- counted_events
- unique_users
type: object
properties:
application:
title: Application
type: object
additionalProperties:
type: string
counted_events:
title: Counted events
type: integer
unique_users:
title: Unique users
type: integer
Application:
description: Application Serializer
required:
@ -6986,6 +6914,82 @@ definitions:
description: Optional Private Key. If this is set, you can use this keypair
for encryption.
type: string
Event:
description: Event Serializer
required:
- action
- app
type: object
properties:
pk:
title: Event uuid
type: string
format: uuid
readOnly: true
user:
title: User
type: object
action:
title: Action
type: string
enum:
- login
- login_failed
- logout
- user_write
- suspicious_request
- password_set
- token_view
- invitation_created
- invitation_used
- authorize_application
- source_linked
- impersonation_started
- impersonation_ended
- policy_execution
- policy_exception
- property_mapping_exception
- model_created
- model_updated
- model_deleted
- update_available
- custom_
app:
title: App
type: string
minLength: 1
context:
title: Context
type: object
client_ip:
title: Client ip
type: string
minLength: 1
x-nullable: true
created:
title: Created
type: string
format: date-time
readOnly: true
EventTopPerUserSerialier:
description: Response object of Event's top_per_user
required:
- application
- counted_events
- unique_users
type: object
properties:
application:
title: Application
type: object
additionalProperties:
type: string
counted_events:
title: Counted events
type: integer
unique_users:
title: Unique users
type: integer
Stage:
title: Stage obj
description: Stage Serializer
@ -7380,6 +7384,11 @@ definitions:
title: Name
type: string
x-nullable: true
execution_logging:
title: Execution logging
description: When this option is enabled, all executions of this policy will
be logged. By default, only execution errors are logged.
type: boolean
__type__:
title: 'type '
type: string

View file

@ -1,9 +1,9 @@
import { DefaultClient } from "./Client";
export class AuditEvent {
//audit/events/top_per_user/?filter_action=authorize_application
export class Event {
// events/events/top_per_user/?filter_action=authorize_application
static topForUser(action: string): Promise<TopNEvent[]> {
return DefaultClient.fetch<TopNEvent[]>(["audit", "events", "top_per_user"], {
return DefaultClient.fetch<TopNEvent[]>(["events", "events", "top_per_user"], {
"filter_action": action,
});
}

View file

@ -9,7 +9,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Monitor").children(
new SidebarItem("Overview", "/administration/overview/"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Events", "/audit/audit"),
new SidebarItem("Events", "/events/log/"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),

View file

@ -1,6 +1,6 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { AuditEvent, TopNEvent } from "../../api/Events";
import { Event, TopNEvent } from "../../api/Events";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Spinner";
@ -16,7 +16,7 @@ export class TopApplicationsTable extends LitElement {
}
firstUpdated(): void {
AuditEvent.topForUser("authorize_application").then(events => this.topN = events);
Event.topForUser("authorize_application").then(events => this.topN = events);
}
renderRow(event: TopNEvent): TemplateResult {