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.serializers import Serializer
|
||||||
from rest_framework.viewsets import ViewSet
|
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]]:
|
def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:
|
||||||
|
|
|
@ -51,7 +51,7 @@ class VersionViewSet(ListModelMixin, GenericViewSet):
|
||||||
|
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self): # pragma: no cover
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: VersionSerializer(many=True)})
|
@swagger_auto_schema(responses={200: VersionSerializer(many=True)})
|
||||||
|
|
|
@ -15,7 +15,7 @@ class WorkerViewSet(ListModelMixin, GenericViewSet):
|
||||||
serializer_class = Serializer
|
serializer_class = Serializer
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self): # pragma: no cover
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def list(self, request: Request) -> Response:
|
def list(self, request: Request) -> Response:
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
"""authentik admin tasks"""
|
"""authentik admin tasks"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from packaging.version import parse
|
||||||
from requests import RequestException, get
|
from requests import RequestException, get
|
||||||
from structlog import get_logger
|
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.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
@ -19,12 +22,24 @@ def update_latest_version(self: MonitoredTask):
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
tag_name = data.get("tag_name")
|
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(
|
self.set_status(
|
||||||
TaskResult(
|
TaskResult(
|
||||||
TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]
|
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:
|
except (RequestException, IndexError) as exc:
|
||||||
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||||
|
|
|
@ -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.admin.api.workers import WorkerViewSet
|
||||||
from authentik.api.v2.config import ConfigsViewSet
|
from authentik.api.v2.config import ConfigsViewSet
|
||||||
from authentik.api.v2.messages import MessagesViewSet
|
from authentik.api.v2.messages import MessagesViewSet
|
||||||
from authentik.audit.api import EventViewSet
|
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.groups import GroupViewSet
|
from authentik.core.api.groups import GroupViewSet
|
||||||
from authentik.core.api.propertymappings import PropertyMappingViewSet
|
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.tokens import TokenViewSet
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||||
|
from authentik.events.api import EventViewSet
|
||||||
from authentik.flows.api import (
|
from authentik.flows.api import (
|
||||||
FlowCacheViewSet,
|
FlowCacheViewSet,
|
||||||
FlowStageBindingViewSet,
|
FlowStageBindingViewSet,
|
||||||
|
@ -96,7 +96,7 @@ router.register("flows/bindings", FlowStageBindingViewSet)
|
||||||
|
|
||||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
|
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
|
||||||
|
|
||||||
router.register("audit/events", EventViewSet)
|
router.register("events/events", EventViewSet)
|
||||||
|
|
||||||
router.register("sources/all", SourceViewSet)
|
router.register("sources/all", SourceViewSet)
|
||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
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 rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
|
|
||||||
from authentik.admin.api.metrics import get_events_per_1h
|
from authentik.admin.api.metrics import get_events_per_1h
|
||||||
from authentik.audit.models import EventAction
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
|
from authentik.events.models import EventAction
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class ApplicationViewSet(ModelViewSet):
|
||||||
get_objects_for_user(request.user, "authentik_core.view_application"),
|
get_objects_for_user(request.user, "authentik_core.view_application"),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
)
|
)
|
||||||
if not request.user.has_perm("authentik_audit.view_event"):
|
if not request.user.has_perm("authentik_events.view_event"):
|
||||||
raise Http404
|
raise Http404
|
||||||
return Response(
|
return Response(
|
||||||
get_events_per_1h(
|
get_events_per_1h(
|
||||||
|
|
|
@ -6,8 +6,8 @@ from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.models import Token
|
from authentik.core.models import Token
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
class TokenSerializer(ModelSerializer):
|
class TokenSerializer(ModelSerializer):
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Property Mapping Evaluator"""
|
"""Property Mapping Evaluator"""
|
||||||
|
from traceback import format_tb
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,3 +21,18 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
if request:
|
if request:
|
||||||
self._context["request"] = request
|
self._context["request"] = request
|
||||||
self._context.update(**kwargs)
|
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()
|
||||||
|
|
|
@ -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 django.views import View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
SESSION_IMPERSONATE_USER,
|
SESSION_IMPERSONATE_USER,
|
||||||
)
|
)
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Audit API Views"""
|
"""Events API Views"""
|
||||||
from django.db.models.aggregates import Count
|
from django.db.models.aggregates import Count
|
||||||
from django.db.models.fields.json import KeyTextTransform
|
from django.db.models.fields.json import KeyTextTransform
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
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.serializers import ModelSerializer, Serializer
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(ModelSerializer):
|
class EventSerializer(ModelSerializer):
|
|
@ -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 functools import partial
|
||||||
from typing import Callable
|
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.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
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.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:
|
class AuditMiddleware:
|
|
@ -63,8 +63,8 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "Audit Event",
|
"verbose_name": "Event",
|
||||||
"verbose_name_plural": "Audit Events",
|
"verbose_name_plural": "Events",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_audit", "0001_initial"),
|
("authentik_events", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -3,11 +3,11 @@ from django.apps.registry import Apps
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
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):
|
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
|
db_alias = schema_editor.connection.alias
|
||||||
for event in Event.objects.all():
|
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
|
# Because event objects cannot be updated, we have to re-create them
|
||||||
event.pk = None
|
event.pk = None
|
||||||
event.user_json = (
|
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._state.adding = True
|
||||||
event.save()
|
event.save()
|
||||||
|
@ -24,7 +24,7 @@ def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_audit", "0002_auto_20200918_2116"),
|
("authentik_events", "0002_auto_20200918_2116"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_audit", "0003_auto_20200917_1155"),
|
("authentik_events", "0003_auto_20200917_1155"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_audit", "0004_auto_20200921_1829"),
|
("authentik_events", "0004_auto_20200921_1829"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_audit", "0005_auto_20201005_2139"),
|
("authentik_events", "0005_auto_20201005_2139"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -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"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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 inspect import getmodule, stack
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import Optional, Union
|
||||||
from uuid import UUID, uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext as _
|
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 structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
|
@ -19,78 +16,14 @@ from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_USER,
|
SESSION_IMPERSONATE_USER,
|
||||||
)
|
)
|
||||||
from authentik.core.models import 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
|
from authentik.lib.utils.http import get_client_ip
|
||||||
|
|
||||||
LOGGER = get_logger("authentik.audit")
|
LOGGER = get_logger("authentik.events")
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class EventAction(models.TextChoices):
|
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 = "login"
|
||||||
LOGIN_FAILED = "login_failed"
|
LOGIN_FAILED = "login_failed"
|
||||||
|
@ -111,15 +44,21 @@ class EventAction(models.TextChoices):
|
||||||
IMPERSONATION_STARTED = "impersonation_started"
|
IMPERSONATION_STARTED = "impersonation_started"
|
||||||
IMPERSONATION_ENDED = "impersonation_ended"
|
IMPERSONATION_ENDED = "impersonation_ended"
|
||||||
|
|
||||||
|
POLICY_EXECUTION = "policy_execution"
|
||||||
|
POLICY_EXCEPTION = "policy_exception"
|
||||||
|
PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
|
||||||
|
|
||||||
MODEL_CREATED = "model_created"
|
MODEL_CREATED = "model_created"
|
||||||
MODEL_UPDATED = "model_updated"
|
MODEL_UPDATED = "model_updated"
|
||||||
MODEL_DELETED = "model_deleted"
|
MODEL_DELETED = "model_deleted"
|
||||||
|
|
||||||
|
UPDATE_AVAILABLE = "update_available"
|
||||||
|
|
||||||
CUSTOM_PREFIX = "custom_"
|
CUSTOM_PREFIX = "custom_"
|
||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
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)
|
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
user = models.JSONField(default=dict)
|
user = models.JSONField(default=dict)
|
||||||
|
@ -151,6 +90,12 @@ class Event(models.Model):
|
||||||
event = Event(action=action, app=app, context=cleaned_kwargs)
|
event = Event(action=action, app=app, context=cleaned_kwargs)
|
||||||
return event
|
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(
|
def from_http(
|
||||||
self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
|
self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
|
||||||
) -> "Event":
|
) -> "Event":
|
||||||
|
@ -185,7 +130,7 @@ class Event(models.Model):
|
||||||
"you may not edit an existing %s" % self._meta.model_name
|
"you may not edit an existing %s" % self._meta.model_name
|
||||||
)
|
)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Created Audit event",
|
"Created Event",
|
||||||
action=self.action,
|
action=self.action,
|
||||||
context=self.context,
|
context=self.context,
|
||||||
client_ip=self.client_ip,
|
client_ip=self.client_ip,
|
||||||
|
@ -195,5 +140,5 @@ class Event(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Audit Event")
|
verbose_name = _("Event")
|
||||||
verbose_name_plural = _("Audit Events")
|
verbose_name_plural = _("Events")
|
|
@ -1,4 +1,4 @@
|
||||||
"""authentik audit signal listener"""
|
"""authentik events signal listener"""
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ from django.contrib.auth.signals import (
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.signals import password_changed
|
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.models import Invitation
|
||||||
from authentik.stages.invitation.signals import invitation_created, invitation_used
|
from authentik.stages.invitation.signals import invitation_created, invitation_used
|
||||||
from authentik.stages.user_write.signals import user_write
|
from authentik.stages.user_write.signals import user_write
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="pf-c-content">
|
<div class="pf-c-content">
|
||||||
<h1>
|
<h1>
|
||||||
<i class="pf-icon pf-icon-catalog"></i>
|
<i class="pf-icon pf-icon-catalog"></i>
|
||||||
{% trans 'Audit Log' %}
|
{% trans 'Event Log' %}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
|
@ -1,15 +1,15 @@
|
||||||
"""audit event tests"""
|
"""events event tests"""
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
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
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
|
|
||||||
class TestAuditEvent(TestCase):
|
class TestEvents(TestCase):
|
||||||
"""Test Audit Event"""
|
"""Test Event"""
|
||||||
|
|
||||||
def test_new_with_model(self):
|
def test_new_with_model(self):
|
||||||
"""Create a new Event passing a model as kwarg"""
|
"""Create a new Event passing a model as kwarg"""
|
|
@ -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"),
|
||||||
|
]
|
|
@ -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 guardian.mixins import PermissionListMixin
|
||||||
|
|
||||||
from authentik.admin.views.utils import SearchListMixin, UserPaginateListMixin
|
from authentik.admin.views.utils import SearchListMixin, UserPaginateListMixin
|
||||||
from authentik.audit.models import Event
|
from authentik.events.models import Event
|
||||||
|
|
||||||
|
|
||||||
class EventListView(
|
class EventListView(
|
||||||
|
@ -17,8 +17,8 @@ class EventListView(
|
||||||
"""Show list of all invitations"""
|
"""Show list of all invitations"""
|
||||||
|
|
||||||
model = Event
|
model = Event
|
||||||
template_name = "audit/list.html"
|
template_name = "events/list.html"
|
||||||
permission_required = "authentik_audit.view_event"
|
permission_required = "authentik_events.view_event"
|
||||||
ordering = "-created"
|
ordering = "-created"
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
|
@ -8,8 +8,8 @@ from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.audit.models import cleanse_dict
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.events.models import cleanse_dict
|
||||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
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 django.views.generic import TemplateView, View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.audit.models import cleanse_dict
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
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.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
|
|
|
@ -80,11 +80,15 @@ class BaseEvaluator:
|
||||||
span: Span
|
span: Span
|
||||||
span.set_data("expression", expression_source)
|
span.set_data("expression", expression_source)
|
||||||
param_keys = self._context.keys()
|
param_keys = self._context.keys()
|
||||||
ast_obj = compile(
|
try:
|
||||||
self.wrap_expression(expression_source, param_keys),
|
ast_obj = compile(
|
||||||
self._filename,
|
self.wrap_expression(expression_source, param_keys),
|
||||||
"exec",
|
self._filename,
|
||||||
)
|
"exec",
|
||||||
|
)
|
||||||
|
except (SyntaxError, ValueError) as exc:
|
||||||
|
self.handle_error(exc, expression_source)
|
||||||
|
raise exc
|
||||||
try:
|
try:
|
||||||
_locals = self._context
|
_locals = self._context
|
||||||
# Yes this is an exec, yes it is potentially bad. Since we limit what variables are
|
# 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
|
exec(ast_obj, self._globals, _locals) # nosec # noqa
|
||||||
result = _locals["result"]
|
result = _locals["result"]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOGGER.warning("Expression error", exc=exc)
|
self.handle_error(exc, expression_source)
|
||||||
raise
|
raise exc
|
||||||
return result
|
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:
|
def validate(self, expression: str) -> bool:
|
||||||
"""Validate expression's syntax, raise ValidationError if Syntax is invalid"""
|
"""Validate expression's syntax, raise ValidationError if Syntax is invalid"""
|
||||||
param_keys = self._context.keys()
|
param_keys = self._context.keys()
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
"""authentik expression policy evaluator"""
|
"""authentik expression policy evaluator"""
|
||||||
from ipaddress import ip_address, ip_network
|
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 django.http import HttpRequest
|
||||||
from structlog import get_logger
|
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.flows.planner import PLAN_CONTEXT_SSO
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
|
|
||||||
|
|
||||||
class PolicyEvaluator(BaseEvaluator):
|
class PolicyEvaluator(BaseEvaluator):
|
||||||
|
@ -18,6 +23,8 @@ class PolicyEvaluator(BaseEvaluator):
|
||||||
|
|
||||||
_messages: List[str]
|
_messages: List[str]
|
||||||
|
|
||||||
|
policy: Optional["ExpressionPolicy"] = None
|
||||||
|
|
||||||
def __init__(self, policy_name: str):
|
def __init__(self, policy_name: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._messages = []
|
self._messages = []
|
||||||
|
@ -45,15 +52,30 @@ class PolicyEvaluator(BaseEvaluator):
|
||||||
self._context["ak_client_ip"] = ip_address(
|
self._context["ak_client_ip"] = ip_address(
|
||||||
get_client_ip(request) or "255.255.255.255"
|
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:
|
def evaluate(self, expression_source: str) -> PolicyResult:
|
||||||
"""Parse and evaluate expression. Policy is expected to return a truthy object.
|
"""Parse and evaluate expression. Policy is expected to return a truthy object.
|
||||||
Messages can be added using 'do ak_message()'."""
|
Messages can be added using 'do ak_message()'."""
|
||||||
try:
|
try:
|
||||||
result = super().evaluate(expression_source)
|
result = super().evaluate(expression_source)
|
||||||
except (ValueError, SyntaxError) as exc:
|
|
||||||
return PolicyResult(False, str(exc))
|
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.warning("Expression error", exc=exc)
|
LOGGER.warning("Expression error", exc=exc)
|
||||||
return PolicyResult(False, str(exc))
|
return PolicyResult(False, str(exc))
|
||||||
|
|
|
@ -31,11 +31,14 @@ class ExpressionPolicy(Policy):
|
||||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||||
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
|
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
|
||||||
evaluator = PolicyEvaluator(self.name)
|
evaluator = PolicyEvaluator(self.name)
|
||||||
|
evaluator.policy = self
|
||||||
evaluator.set_policy_request(request)
|
evaluator.set_policy_request(request)
|
||||||
return evaluator.evaluate(self.expression)
|
return evaluator.evaluate(self.expression)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -3,7 +3,9 @@ from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
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.evaluator import PolicyEvaluator
|
||||||
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +15,14 @@ class TestEvaluator(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.request = PolicyRequest(user=get_anonymous_user())
|
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):
|
def test_valid(self):
|
||||||
"""test simple value expression"""
|
"""test simple value expression"""
|
||||||
template = "return True"
|
template = "return True"
|
||||||
|
@ -37,6 +47,12 @@ class TestEvaluator(TestCase):
|
||||||
result = evaluator.evaluate(template)
|
result = evaluator.evaluate(template)
|
||||||
self.assertEqual(result.passing, False)
|
self.assertEqual(result.passing, False)
|
||||||
self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
|
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):
|
def test_undefined(self):
|
||||||
"""test undefined result"""
|
"""test undefined result"""
|
||||||
|
@ -46,6 +62,12 @@ class TestEvaluator(TestCase):
|
||||||
result = evaluator.evaluate(template)
|
result = evaluator.evaluate(template)
|
||||||
self.assertEqual(result.passing, False)
|
self.assertEqual(result.passing, False)
|
||||||
self.assertEqual(result.messages, ("name 'foo' is not defined",))
|
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):
|
def test_validate(self):
|
||||||
"""test validate"""
|
"""test validate"""
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
||||||
from authentik.lib.widgets import GroupedModelChoiceField
|
from authentik.lib.widgets import GroupedModelChoiceField
|
||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||||
|
|
||||||
GENERAL_FIELDS = ["name"]
|
GENERAL_FIELDS = ["name", "execution_logging"]
|
||||||
GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
|
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)
|
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()
|
objects = InheritanceAutoManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -8,6 +8,7 @@ from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.policies.exceptions import PolicyException
|
from authentik.policies.exceptions import PolicyException
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
@ -48,40 +49,48 @@ class PolicyProcess(Process):
|
||||||
|
|
||||||
def execute(self) -> PolicyResult:
|
def execute(self) -> PolicyResult:
|
||||||
"""Run actual policy, returns result"""
|
"""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(
|
with Hub.current.start_span(
|
||||||
op="policy.process.execute",
|
op="policy.process.execute",
|
||||||
) as span:
|
) as span:
|
||||||
span: Span
|
span: Span
|
||||||
span.set_data("policy", self.binding.policy)
|
span.set_data("policy", self.binding.policy)
|
||||||
span.set_data("request", self.request)
|
span.set_data("request", self.request)
|
||||||
LOGGER.debug(
|
self.connection.send(self.execute())
|
||||||
"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())
|
|
||||||
|
|
|
@ -7,13 +7,14 @@ from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||||
|
from authentik.policies.tests.test_process import clear_policy_cache
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyEngine(TestCase):
|
class TestPolicyEngine(TestCase):
|
||||||
"""PolicyEngine tests"""
|
"""PolicyEngine tests"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
cache.clear()
|
clear_policy_cache()
|
||||||
self.user = User.objects.create_user(username="policyuser")
|
self.user = User.objects.create_user(username="policyuser")
|
||||||
self.policy_false = DummyPolicy.objects.create(
|
self.policy_false = DummyPolicy.objects.create(
|
||||||
result=False, wait_min=0, wait_max=1
|
result=False, wait_min=0, wait_max=1
|
||||||
|
@ -84,10 +85,18 @@ class TestPolicyEngine(TestCase):
|
||||||
def test_engine_cache(self):
|
def test_engine_cache(self):
|
||||||
"""Ensure empty policy list passes"""
|
"""Ensure empty policy list passes"""
|
||||||
pbm = PolicyBindingModel.objects.create()
|
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)
|
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(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(engine.build().passing, False)
|
||||||
self.assertEqual(len(cache.keys("policy_*")), 1)
|
self.assertEqual(
|
||||||
|
len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1
|
||||||
|
)
|
||||||
|
|
|
@ -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"""
|
"""policy structures"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
@ -11,6 +12,7 @@ if TYPE_CHECKING:
|
||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class PolicyRequest:
|
class PolicyRequest:
|
||||||
"""Data-class to hold policy request data"""
|
"""Data-class to hold policy request data"""
|
||||||
|
|
||||||
|
@ -20,6 +22,7 @@ class PolicyRequest:
|
||||||
context: Dict[str, str]
|
context: Dict[str, str]
|
||||||
|
|
||||||
def __init__(self, user: User):
|
def __init__(self, user: User):
|
||||||
|
super().__init__()
|
||||||
self.user = user
|
self.user = user
|
||||||
self.http_request = None
|
self.http_request = None
|
||||||
self.obj = None
|
self.obj = None
|
||||||
|
@ -29,6 +32,7 @@ class PolicyRequest:
|
||||||
return f"<PolicyRequest user={self.user}>"
|
return f"<PolicyRequest user={self.user}>"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class PolicyResult:
|
class PolicyResult:
|
||||||
"""Small data-class to hold policy results"""
|
"""Small data-class to hold policy results"""
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@ class PolicyResult:
|
||||||
source_results: Optional[List["PolicyResult"]]
|
source_results: Optional[List["PolicyResult"]]
|
||||||
|
|
||||||
def __init__(self, passing: bool, *messages: str):
|
def __init__(self, passing: bool, *messages: str):
|
||||||
|
super().__init__()
|
||||||
self.passing = passing
|
self.passing = passing
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
self.source_policy = None
|
self.source_policy = None
|
||||||
|
@ -49,5 +54,5 @@ class PolicyResult:
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.messages:
|
if self.messages:
|
||||||
return f"PolicyResult passing={self.passing} messages={self.messages}"
|
return f"<PolicyResult passing={self.passing} messages={self.messages}>"
|
||||||
return f"PolicyResult passing={self.passing}"
|
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 django.utils import timezone
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
PLAN_CONTEXT_APPLICATION,
|
PLAN_CONTEXT_APPLICATION,
|
||||||
|
|
|
@ -11,8 +11,8 @@ from django.views import View
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.models import Application, Provider
|
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.models import in_memory_stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
PLAN_CONTEXT_APPLICATION,
|
PLAN_CONTEXT_APPLICATION,
|
||||||
|
|
|
@ -86,7 +86,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.humanize",
|
"django.contrib.humanize",
|
||||||
"authentik.admin.apps.AuthentikAdminConfig",
|
"authentik.admin.apps.AuthentikAdminConfig",
|
||||||
"authentik.api.apps.AuthentikAPIConfig",
|
"authentik.api.apps.AuthentikAPIConfig",
|
||||||
"authentik.audit.apps.AuthentikAuditConfig",
|
"authentik.events.apps.AuthentikEventsConfig",
|
||||||
"authentik.crypto.apps.AuthentikCryptoConfig",
|
"authentik.crypto.apps.AuthentikCryptoConfig",
|
||||||
"authentik.flows.apps.AuthentikFlowsConfig",
|
"authentik.flows.apps.AuthentikFlowsConfig",
|
||||||
"authentik.outposts.apps.AuthentikOutpostConfig",
|
"authentik.outposts.apps.AuthentikOutpostConfig",
|
||||||
|
@ -180,7 +180,7 @@ MIDDLEWARE = [
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"authentik.core.middleware.RequestIDMiddleware",
|
"authentik.core.middleware.RequestIDMiddleware",
|
||||||
"authentik.audit.middleware.AuditMiddleware",
|
"authentik.events.middleware.AuditMiddleware",
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
|
|
@ -10,8 +10,8 @@ from django.utils.translation import gettext as _
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.models import User
|
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.models import Flow, in_memory_stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
PLAN_CONTEXT_PENDING_USER,
|
PLAN_CONTEXT_PENDING_USER,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""OAuth Stages"""
|
"""OAuth Stages"""
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
from authentik.audit.models import Event, EventAction
|
|
||||||
from authentik.core.models import User
|
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.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.sources.oauth.models import UserOAuthSourceConnection
|
from authentik.sources.oauth.models import UserOAuthSourceConnection
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.views import View
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
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
|
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.views.generic import TemplateView
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
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
|
from authentik.stages.otp_time.models import OTPTimeStage
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
parameters: []
|
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/:
|
/core/applications/:
|
||||||
get:
|
get:
|
||||||
operationId: core_applications_list
|
operationId: core_applications_list
|
||||||
|
@ -968,6 +862,112 @@ paths:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
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/:
|
/flows/bindings/:
|
||||||
get:
|
get:
|
||||||
operationId: flows_bindings_list
|
operationId: flows_bindings_list
|
||||||
|
@ -6729,78 +6729,6 @@ definitions:
|
||||||
title: Outdated
|
title: Outdated
|
||||||
type: boolean
|
type: boolean
|
||||||
readOnly: true
|
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:
|
Application:
|
||||||
description: Application Serializer
|
description: Application Serializer
|
||||||
required:
|
required:
|
||||||
|
@ -6986,6 +6914,82 @@ definitions:
|
||||||
description: Optional Private Key. If this is set, you can use this keypair
|
description: Optional Private Key. If this is set, you can use this keypair
|
||||||
for encryption.
|
for encryption.
|
||||||
type: string
|
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:
|
Stage:
|
||||||
title: Stage obj
|
title: Stage obj
|
||||||
description: Stage Serializer
|
description: Stage Serializer
|
||||||
|
@ -7380,6 +7384,11 @@ definitions:
|
||||||
title: Name
|
title: Name
|
||||||
type: string
|
type: string
|
||||||
x-nullable: true
|
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__:
|
__type__:
|
||||||
title: 'type '
|
title: 'type '
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { DefaultClient } from "./Client";
|
import { DefaultClient } from "./Client";
|
||||||
|
|
||||||
export class AuditEvent {
|
export class Event {
|
||||||
//audit/events/top_per_user/?filter_action=authorize_application
|
// events/events/top_per_user/?filter_action=authorize_application
|
||||||
static topForUser(action: string): Promise<TopNEvent[]> {
|
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,
|
"filter_action": action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
||||||
new SidebarItem("Monitor").children(
|
new SidebarItem("Monitor").children(
|
||||||
new SidebarItem("Overview", "/administration/overview/"),
|
new SidebarItem("Overview", "/administration/overview/"),
|
||||||
new SidebarItem("System Tasks", "/administration/tasks/"),
|
new SidebarItem("System Tasks", "/administration/tasks/"),
|
||||||
new SidebarItem("Events", "/audit/audit"),
|
new SidebarItem("Events", "/events/log/"),
|
||||||
).when((): Promise<boolean> => {
|
).when((): Promise<boolean> => {
|
||||||
return User.me().then(u => u.is_superuser);
|
return User.me().then(u => u.is_superuser);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { gettext } from "django";
|
import { gettext } from "django";
|
||||||
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
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 { COMMON_STYLES } from "../../common/styles";
|
||||||
|
|
||||||
import "../../elements/Spinner";
|
import "../../elements/Spinner";
|
||||||
|
@ -16,7 +16,7 @@ export class TopApplicationsTable extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
AuditEvent.topForUser("authorize_application").then(events => this.topN = events);
|
Event.topForUser("authorize_application").then(events => this.topN = events);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRow(event: TopNEvent): TemplateResult {
|
renderRow(event: TopNEvent): TemplateResult {
|
||||||
|
|
Reference in New Issue