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:
parent
4d88dcff08
commit
a4dc6d13b5
|
@ -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]]:
|
||||
|
|
|
@ -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)})
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
76
authentik/admin/tests/test_tasks.py
Normal file
76
authentik/admin/tests/test_tasks.py
Normal 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()
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
|
@ -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"),
|
||||
]
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
56
authentik/core/tests/test_property_mapping.py
Normal file
56
authentik/core/tests/test_property_mapping.py
Normal 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")
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
16
authentik/events/apps.py
Normal 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")
|
|
@ -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:
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_audit", "0001_initial"),
|
||||
("authentik_events", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -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 = [
|
|
@ -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 = [
|
|
@ -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 = [
|
|
@ -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 = [
|
41
authentik/events/migrations/0007_auto_20201215_0939.py
Normal file
41
authentik/events/migrations/0007_auto_20201215_0939.py
Normal 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"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
42
authentik/events/migrations/0008_auto_20201220_1651.py
Normal file
42
authentik/events/migrations/0008_auto_20201220_1651.py
Normal 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"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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")
|
|
@ -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
|
|
@ -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>
|
|
@ -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
9
authentik/events/urls.py
Normal 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
86
authentik/events/utils.py
Normal 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
|
|
@ -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 = [
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -80,11 +80,15 @@ class BaseEvaluator:
|
|||
span: Span
|
||||
span.set_data("expression", expression_source)
|
||||
param_keys = self._context.keys()
|
||||
ast_obj = compile(
|
||||
self.wrap_expression(expression_source, param_keys),
|
||||
self._filename,
|
||||
"exec",
|
||||
)
|
||||
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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
|
@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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,40 +49,48 @@ class PolicyProcess(Process):
|
|||
|
||||
def execute(self) -> PolicyResult:
|
||||
"""Run actual policy, returns result"""
|
||||
LOGGER.debug(
|
||||
"P_ENG(proc): Running policy",
|
||||
policy=self.binding.policy,
|
||||
user=self.request.user,
|
||||
process="PolicyProcess",
|
||||
)
|
||||
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))
|
||||
policy_result.source_policy = self.binding.policy
|
||||
# Invert result if policy.negate is set
|
||||
if self.binding.negate:
|
||||
policy_result.passing = not policy_result.passing
|
||||
LOGGER.debug(
|
||||
"P_ENG(proc): Finished",
|
||||
policy=self.binding.policy,
|
||||
result=policy_result,
|
||||
process="PolicyProcess",
|
||||
passing=policy_result.passing,
|
||||
user=self.request.user,
|
||||
)
|
||||
key = cache_key(self.binding, self.request)
|
||||
cache.set(key, policy_result)
|
||||
LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
|
||||
return policy_result
|
||||
|
||||
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)
|
||||
LOGGER.debug(
|
||||
"P_ENG(proc): Running policy",
|
||||
policy=self.binding.policy,
|
||||
user=self.request.user,
|
||||
process="PolicyProcess",
|
||||
)
|
||||
try:
|
||||
policy_result = self.binding.policy.passes(self.request)
|
||||
except PolicyException as exc:
|
||||
LOGGER.debug("P_ENG(proc): error", exc=exc)
|
||||
policy_result = PolicyResult(False, str(exc))
|
||||
policy_result.source_policy = self.binding.policy
|
||||
# Invert result if policy.negate is set
|
||||
if self.binding.negate:
|
||||
policy_result.passing = not policy_result.passing
|
||||
LOGGER.debug(
|
||||
"P_ENG(proc): Finished",
|
||||
policy=self.binding.policy,
|
||||
result=policy_result,
|
||||
process="PolicyProcess",
|
||||
passing=policy_result.passing,
|
||||
user=self.request.user,
|
||||
)
|
||||
key = cache_key(self.binding, self.request)
|
||||
cache.set(key, policy_result)
|
||||
LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
|
||||
return policy_result
|
||||
|
||||
def run(self):
|
||||
"""Task wrapper to run policy checking"""
|
||||
self.connection.send(self.execute())
|
||||
self.connection.send(self.execute())
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
105
authentik/policies/tests/test_process.py
Normal file
105
authentik/policies/tests/test_process.py
Normal 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
|
|
@ -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}>"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
21
lifecycle/system_migrations/to_0_13_events..py
Normal file
21
lifecycle/system_migrations/to_0_13_events..py
Normal 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()
|
365
swagger.yaml
365
swagger.yaml
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
@ -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 {
|
||||
|
|
Reference in a new issue