enterprise: add full audit log [AUTH-458] (#8177)
* enterprise: add full audit log Signed-off-by: Jens Langhammer <jens@goauthentik.io> * delegate enabled check to apps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * move audit middleware to separate app Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanse before diff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make cleanse include a hash of the values Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix sentry error during lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only use start of hash Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't use deepdiff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add diff ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix info for dict Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update release notes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * enable audit logging for tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix startup with tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include first 4 chars of raw value? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only log asterisks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -7,6 +7,8 @@ from dacite.config import Config
|
||||
from dacite.core import from_dict
|
||||
from dacite.exceptions import DaciteError
|
||||
from deepmerge import always_merger
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.models import Model
|
||||
from django.db.models.query_utils import Q
|
||||
@ -58,9 +60,11 @@ def excluded_models() -> list[type[Model]]:
|
||||
from django.contrib.auth.models import User as DjangoUser
|
||||
|
||||
return (
|
||||
Tenant,
|
||||
# Django only classes
|
||||
DjangoUser,
|
||||
DjangoGroup,
|
||||
ContentType,
|
||||
Permission,
|
||||
# Base classes
|
||||
Provider,
|
||||
Source,
|
||||
@ -77,6 +81,7 @@ def excluded_models() -> list[type[Model]]:
|
||||
LicenseUsage,
|
||||
SCIMGroup,
|
||||
SCIMUser,
|
||||
Tenant,
|
||||
)
|
||||
|
||||
|
||||
|
@ -47,7 +47,14 @@ USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||
|
||||
|
||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
|
||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
||||
# used_by API that allows models to specify if they shadow an object
|
||||
# for example the proxy provider which is built on top of an oauth provider
|
||||
"authentik_used_by_shadows",
|
||||
# List fields for which changes are not logged (due to them having dedicated objects)
|
||||
# for example user's password and last_login
|
||||
"authentik_signals_ignored_fields",
|
||||
)
|
||||
|
||||
|
||||
def default_token_duration():
|
||||
@ -278,6 +285,14 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||
("assign_user_permissions", _("Can assign permissions to users")),
|
||||
("unassign_user_permissions", _("Can unassign permissions from users")),
|
||||
]
|
||||
authentik_signals_ignored_fields = [
|
||||
# Logged by the events `password_set`
|
||||
# the `password_set` action/signal doesn't currently convey which user
|
||||
# initiated the password change, so for now we'll log two actions
|
||||
# ("password", "password_change_date"),
|
||||
# Logged by `login`
|
||||
("last_login",),
|
||||
]
|
||||
|
||||
|
||||
class Provider(SerializerModel):
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""Enterprise app config"""
|
||||
from functools import lru_cache
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
@ -26,7 +24,6 @@ class AuthentikEnterpriseConfig(EnterpriseConfig):
|
||||
"""Return true if enterprise is enabled and valid"""
|
||||
return self.check_enabled() or settings.TEST
|
||||
|
||||
@lru_cache()
|
||||
def check_enabled(self):
|
||||
"""Actual enterprise check, cached"""
|
||||
from authentik.enterprise.models import LicenseKey
|
||||
|
0
authentik/enterprise/audit/__init__.py
Normal file
0
authentik/enterprise/audit/__init__.py
Normal file
19
authentik/enterprise/audit/apps.py
Normal file
19
authentik/enterprise/audit/apps.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""Enterprise app config"""
|
||||
from django.conf import settings
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEnterpriseAuditConfig(EnterpriseConfig):
|
||||
"""Enterprise app config"""
|
||||
|
||||
name = "authentik.enterprise.audit"
|
||||
label = "authentik_enterprise_audit"
|
||||
verbose_name = "authentik Enterprise.Audit"
|
||||
default = True
|
||||
|
||||
def reconcile_global_install_middleware(self):
|
||||
"""Install enterprise audit middleware"""
|
||||
orig_import = "authentik.events.middleware.AuditMiddleware"
|
||||
new_import = "authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware"
|
||||
settings.MIDDLEWARE = [new_import if x == orig_import else x for x in settings.MIDDLEWARE]
|
121
authentik/enterprise/audit/middleware.py
Normal file
121
authentik/enterprise/audit/middleware.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""Enterprise audit middleware"""
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
|
||||
from django.apps.registry import apps
|
||||
from django.core.files import File
|
||||
from django.db import connection
|
||||
from django.db.models import Model
|
||||
from django.db.models.expressions import BaseExpression, Combinable
|
||||
from django.db.models.signals import post_init
|
||||
from django.http import HttpRequest
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.middleware import AuditMiddleware, should_log_model
|
||||
from authentik.events.utils import cleanse_dict, sanitize_item
|
||||
|
||||
|
||||
class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
"""Enterprise audit middleware"""
|
||||
|
||||
_enabled = None
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Lazy check if audit logging is enabled"""
|
||||
if self._enabled is None:
|
||||
self._enabled = apps.get_app_config("authentik_enterprise").enabled()
|
||||
return self._enabled
|
||||
|
||||
def connect(self, request: HttpRequest):
|
||||
super().connect(request)
|
||||
if not self.enabled:
|
||||
return
|
||||
user = getattr(request, "user", self.anonymous_user)
|
||||
if not user.is_authenticated:
|
||||
user = self.anonymous_user
|
||||
if not hasattr(request, "request_id"):
|
||||
return
|
||||
post_init.connect(
|
||||
partial(self.post_init_handler, user=user, request=request),
|
||||
dispatch_uid=request.request_id,
|
||||
weak=False,
|
||||
)
|
||||
|
||||
def disconnect(self, request: HttpRequest):
|
||||
super().disconnect(request)
|
||||
if not self.enabled:
|
||||
return
|
||||
if not hasattr(request, "request_id"):
|
||||
return
|
||||
post_init.disconnect(dispatch_uid=request.request_id)
|
||||
|
||||
def serialize_simple(self, model: Model) -> dict:
|
||||
"""Serialize a model in a very simple way. No ForeginKeys or other relationships are
|
||||
resolved"""
|
||||
data = {}
|
||||
deferred_fields = model.get_deferred_fields()
|
||||
for field in model._meta.concrete_fields:
|
||||
value = None
|
||||
if field.get_attname() in deferred_fields:
|
||||
continue
|
||||
|
||||
field_value = getattr(model, field.attname)
|
||||
if isinstance(value, File):
|
||||
field_value = value.name
|
||||
|
||||
# If current field value is an expression, we are not evaluating it
|
||||
if isinstance(field_value, (BaseExpression, Combinable)):
|
||||
continue
|
||||
field_value = field.to_python(field_value)
|
||||
data[field.name] = deepcopy(field_value)
|
||||
return cleanse_dict(data)
|
||||
|
||||
def diff(self, before: dict, after: dict) -> dict:
|
||||
"""Generate diff between dicts"""
|
||||
diff = {}
|
||||
for key, value in before.items():
|
||||
if after.get(key) != value:
|
||||
diff[key] = {"previous_value": value, "new_value": after.get(key)}
|
||||
return sanitize_item(diff)
|
||||
|
||||
def post_init_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_):
|
||||
"""post_init django model handler"""
|
||||
if not should_log_model(instance):
|
||||
return
|
||||
if hasattr(instance, "_previous_state"):
|
||||
return
|
||||
before = len(connection.queries)
|
||||
setattr(instance, "_previous_state", self.serialize_simple(instance))
|
||||
after = len(connection.queries)
|
||||
if after > before:
|
||||
raise AssertionError("More queries generated by serialize_simple")
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def post_save_handler(
|
||||
self,
|
||||
user: User,
|
||||
request: HttpRequest,
|
||||
sender,
|
||||
instance: Model,
|
||||
created: bool,
|
||||
thread_kwargs: dict | None = None,
|
||||
**_,
|
||||
):
|
||||
if not should_log_model(instance):
|
||||
return None
|
||||
thread_kwargs = {}
|
||||
if hasattr(instance, "_previous_state") or created:
|
||||
prev_state = getattr(instance, "_previous_state", {})
|
||||
# Get current state
|
||||
new_state = self.serialize_simple(instance)
|
||||
diff = self.diff(prev_state, new_state)
|
||||
thread_kwargs["diff"] = diff
|
||||
if not created:
|
||||
ignored_field_sets = getattr(instance._meta, "authentik_signals_ignored_fields", [])
|
||||
for field_set in ignored_field_sets:
|
||||
if set(diff.keys()) == set(field_set):
|
||||
return None
|
||||
return super().post_save_handler(
|
||||
user, request, sender, instance, created, thread_kwargs, **_
|
||||
)
|
@ -12,5 +12,6 @@ CELERY_BEAT_SCHEDULE = {
|
||||
}
|
||||
|
||||
TENANT_APPS = [
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.providers.rac",
|
||||
]
|
||||
|
@ -10,52 +10,36 @@ from django.db.models import Model
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from guardian.models import UserObjectPermission
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.models import (
|
||||
AuthenticatedSession,
|
||||
Group,
|
||||
PropertyMapping,
|
||||
Provider,
|
||||
Source,
|
||||
User,
|
||||
UserSourceConnection,
|
||||
)
|
||||
from authentik.blueprints.v1.importer import excluded_models
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.enterprise.providers.rac.models import ConnectionToken
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.flows.models import FlowToken, Stage
|
||||
from authentik.lib.sentry import before_send
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.outposts.models import OutpostServiceConnection
|
||||
from authentik.policies.models import Policy, PolicyBindingModel
|
||||
from authentik.policies.reputation.models import Reputation
|
||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
|
||||
from authentik.stages.authenticator_static.models import StaticToken
|
||||
|
||||
IGNORED_MODELS = (
|
||||
Event,
|
||||
Notification,
|
||||
UserObjectPermission,
|
||||
AuthenticatedSession,
|
||||
StaticToken,
|
||||
Session,
|
||||
FlowToken,
|
||||
Provider,
|
||||
Source,
|
||||
PropertyMapping,
|
||||
UserSourceConnection,
|
||||
Stage,
|
||||
OutpostServiceConnection,
|
||||
Policy,
|
||||
PolicyBindingModel,
|
||||
AuthorizationCode,
|
||||
AccessToken,
|
||||
RefreshToken,
|
||||
SCIMUser,
|
||||
SCIMGroup,
|
||||
Reputation,
|
||||
ConnectionToken,
|
||||
IGNORED_MODELS = tuple(
|
||||
excluded_models()
|
||||
+ (
|
||||
Event,
|
||||
Notification,
|
||||
UserObjectPermission,
|
||||
StaticToken,
|
||||
Session,
|
||||
AuthorizationCode,
|
||||
AccessToken,
|
||||
RefreshToken,
|
||||
SCIMUser,
|
||||
SCIMGroup,
|
||||
Reputation,
|
||||
ConnectionToken,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -96,9 +80,11 @@ class AuditMiddleware:
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
anonymous_user: User = None
|
||||
logger: BoundLogger
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
self.logger = get_logger().bind()
|
||||
|
||||
def _ensure_fallback_user(self):
|
||||
"""Defer fetching anonymous user until we have to"""
|
||||
@ -116,21 +102,18 @@ class AuditMiddleware:
|
||||
user = self.anonymous_user
|
||||
if not hasattr(request, "request_id"):
|
||||
return
|
||||
post_save_handler = partial(self.post_save_handler, user=user, request=request)
|
||||
pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
|
||||
m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
|
||||
post_save.connect(
|
||||
post_save_handler,
|
||||
partial(self.post_save_handler, user=user, request=request),
|
||||
dispatch_uid=request.request_id,
|
||||
weak=False,
|
||||
)
|
||||
pre_delete.connect(
|
||||
pre_delete_handler,
|
||||
partial(self.pre_delete_handler, user=user, request=request),
|
||||
dispatch_uid=request.request_id,
|
||||
weak=False,
|
||||
)
|
||||
m2m_changed.connect(
|
||||
m2m_changed_handler,
|
||||
partial(self.m2m_changed_handler, user=user, request=request),
|
||||
dispatch_uid=request.request_id,
|
||||
weak=False,
|
||||
)
|
||||
@ -173,19 +156,27 @@ class AuditMiddleware:
|
||||
)
|
||||
thread.run()
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=too-many-arguments
|
||||
def post_save_handler(
|
||||
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||
self,
|
||||
user: User,
|
||||
request: HttpRequest,
|
||||
sender,
|
||||
instance: Model,
|
||||
created: bool,
|
||||
thread_kwargs: Optional[dict] = None,
|
||||
**_,
|
||||
):
|
||||
"""Signal handler for all object's post_save"""
|
||||
if not should_log_model(instance):
|
||||
return
|
||||
|
||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||
EventNewThread(action, request, user=user, model=model_to_dict(instance)).run()
|
||||
thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
|
||||
thread.kwargs.update(thread_kwargs or {})
|
||||
thread.run()
|
||||
|
||||
@staticmethod
|
||||
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
|
||||
def pre_delete_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_):
|
||||
"""Signal handler for all object's pre_delete"""
|
||||
if not should_log_model(instance): # pragma: no cover
|
||||
return
|
||||
@ -197,9 +188,8 @@ class AuditMiddleware:
|
||||
model=model_to_dict(instance),
|
||||
).run()
|
||||
|
||||
@staticmethod
|
||||
def m2m_changed_handler(
|
||||
user: User, request: HttpRequest, sender, instance: Model, action: str, **_
|
||||
self, user: User, request: HttpRequest, sender, instance: Model, action: str, **_
|
||||
):
|
||||
"""Signal handler for all object's m2m_changed"""
|
||||
if action not in ["pre_add", "pre_remove", "post_clear"]:
|
||||
|
@ -66,7 +66,8 @@ class TestEvents(TestCase):
|
||||
|
||||
def test_from_http_clean_querystring(self):
|
||||
"""Test cleansing query string"""
|
||||
request = self.factory.get(f"/?token={generate_id()}")
|
||||
token = generate_id()
|
||||
request = self.factory.get(f"/?token={token}")
|
||||
event = Event.new("unittest").from_http(request)
|
||||
self.assertEqual(
|
||||
event.context,
|
||||
@ -82,7 +83,8 @@ class TestEvents(TestCase):
|
||||
|
||||
def test_from_http_clean_querystring_flow(self):
|
||||
"""Test cleansing query string (nested query string like flow executor)"""
|
||||
nested_qs = {"token": generate_id()}
|
||||
token = generate_id()
|
||||
nested_qs = {"token": token}
|
||||
request = self.factory.get(f"/?{QS_QUERY}={urlencode(nested_qs)}")
|
||||
event = Event.new("unittest").from_http(request)
|
||||
self.assertEqual(
|
||||
|
@ -28,7 +28,7 @@ from authentik.policies.types import PolicyRequest
|
||||
|
||||
# Special keys which are *not* cleaned, even when the default filter
|
||||
# is matched
|
||||
ALLOWED_SPECIAL_KEYS = re.compile("passing", flags=re.I)
|
||||
ALLOWED_SPECIAL_KEYS = re.compile("passing|password_change_date", flags=re.I)
|
||||
|
||||
|
||||
def cleanse_item(key: str, value: Any) -> Any:
|
||||
@ -40,13 +40,13 @@ def cleanse_item(key: str, value: Any) -> Any:
|
||||
value[idx] = cleanse_item(key, item)
|
||||
return value
|
||||
try:
|
||||
if SafeExceptionReporterFilter.hidden_settings.search(
|
||||
key
|
||||
) and not ALLOWED_SPECIAL_KEYS.search(key):
|
||||
return SafeExceptionReporterFilter.cleansed_substitute
|
||||
if not SafeExceptionReporterFilter.hidden_settings.search(key):
|
||||
return value
|
||||
if ALLOWED_SPECIAL_KEYS.search(key):
|
||||
return value
|
||||
return SafeExceptionReporterFilter.cleansed_substitute
|
||||
except TypeError: # pragma: no cover
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def cleanse_dict(source: dict[Any, Any]) -> dict[Any, Any]:
|
||||
|
@ -60,8 +60,6 @@ def sentry_init(**sentry_init_kwargs):
|
||||
},
|
||||
}
|
||||
kwargs.update(**sentry_init_kwargs)
|
||||
if settings.DEBUG:
|
||||
kwargs["spotlight"] = True
|
||||
# pylint: disable=abstract-class-instantiated
|
||||
sentry_sdk_init(
|
||||
dsn=CONFIG.get("error_reporting.sentry_dsn"),
|
||||
|
@ -136,6 +136,7 @@ class TestPolicyProcess(TestCase):
|
||||
http_request.user = self.user
|
||||
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
|
||||
|
||||
password = generate_id()
|
||||
request = PolicyRequest(self.user)
|
||||
request.set_http_request(http_request)
|
||||
request.context = {
|
||||
@ -144,7 +145,7 @@ class TestPolicyProcess(TestCase):
|
||||
"list": ["foo", "bar"],
|
||||
"tuple": ("foo", "bar"),
|
||||
"set": {"foo", "bar"},
|
||||
"password": generate_id(),
|
||||
"password": password,
|
||||
}
|
||||
}
|
||||
response = PolicyProcess(binding, request, None).execute()
|
||||
|
@ -381,7 +381,7 @@ env = get_env()
|
||||
_ERROR_REPORTING = CONFIG.get_bool("error_reporting.enabled", False)
|
||||
if _ERROR_REPORTING:
|
||||
sentry_env = CONFIG.get("error_reporting.environment", "customer")
|
||||
sentry_init()
|
||||
sentry_init(spotlight=DEBUG)
|
||||
set_tag("authentik.uuid", sha512(str(SECRET_KEY).encode("ascii")).hexdigest()[:16])
|
||||
|
||||
|
||||
|
@ -3647,6 +3647,7 @@
|
||||
"authentik.blueprints",
|
||||
"authentik.core",
|
||||
"authentik.enterprise",
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.providers.rac"
|
||||
],
|
||||
"title": "App",
|
||||
|
1
poetry.lock
generated
1
poetry.lock
generated
@ -3362,7 +3362,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
|
@ -154,6 +154,7 @@ lxml = [
|
||||
# 4.9.x works with previous libxml2 versions, which is what we get on linux
|
||||
{ version = "4.9.4", platform = "linux" },
|
||||
]
|
||||
jsonpatch = "*"
|
||||
opencontainers = { extras = ["reggie"], version = "*" }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
@ -181,7 +182,6 @@ webauthn = "*"
|
||||
wsproto = "*"
|
||||
xmlsec = "*"
|
||||
zxcvbn = "*"
|
||||
jsonpatch = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
bandit = "*"
|
||||
|
@ -29362,6 +29362,7 @@ components:
|
||||
- authentik.blueprints
|
||||
- authentik.core
|
||||
- authentik.enterprise
|
||||
- authentik.enterprise.audit
|
||||
- authentik.enterprise.providers.rac
|
||||
type: string
|
||||
description: |-
|
||||
@ -29415,6 +29416,7 @@ components:
|
||||
* `authentik.blueprints` - authentik Blueprints
|
||||
* `authentik.core` - authentik Core
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
AppleChallengeResponseRequest:
|
||||
type: object
|
||||
@ -32415,6 +32417,7 @@ components:
|
||||
* `authentik.blueprints` - authentik Blueprints
|
||||
* `authentik.core` - authentik Core
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
model:
|
||||
allOf:
|
||||
@ -32617,6 +32620,7 @@ components:
|
||||
* `authentik.blueprints` - authentik Blueprints
|
||||
* `authentik.core` - authentik Core
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
model:
|
||||
allOf:
|
||||
@ -38243,6 +38247,7 @@ components:
|
||||
* `authentik.blueprints` - authentik Blueprints
|
||||
* `authentik.core` - authentik Core
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
model:
|
||||
allOf:
|
||||
|
@ -154,6 +154,12 @@ export class EventViewPage extends AKElement {
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl">
|
||||
<ak-event-info .event=${this.event}></ak-event-info>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Raw event info")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<pre>${JSON.stringify(this.event, null, 4)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@ -78,6 +79,7 @@ export class EventInfo extends AKElement {
|
||||
PFButton,
|
||||
PFFlex,
|
||||
PFCard,
|
||||
PFTable,
|
||||
PFList,
|
||||
PFDescriptionList,
|
||||
css`
|
||||
@ -243,11 +245,52 @@ export class EventInfo extends AKElement {
|
||||
}
|
||||
|
||||
renderModelChanged() {
|
||||
const diff = this.event.context.diff as unknown as {
|
||||
[key: string]: { new_value: unknown; previous_value: unknown };
|
||||
};
|
||||
let diffBody = html``;
|
||||
if (diff) {
|
||||
diffBody = html`<div class="pf-l-flex__item">
|
||||
<div class="pf-c-card__title">${msg("Changes made:")}</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">${msg("Key")}</th>
|
||||
<th role="columnheader" scope="col">${msg("Previous value")}</th>
|
||||
<th role="columnheader" scope="col">${msg("New value")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
${Object.keys(diff).map((key) => {
|
||||
return html` <tr role="row">
|
||||
<td role="cell"><pre>${key}</pre></td>
|
||||
<td role="cell">
|
||||
<pre>
|
||||
${JSON.stringify(diff[key].previous_value, null, 4)}</pre
|
||||
>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<pre>${JSON.stringify(diff[key].new_value, null, 4)}</pre>
|
||||
</td>
|
||||
</tr>`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
return html`
|
||||
<div class="pf-c-card__title">${msg("Affected model:")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.getModelInfo(this.event.context?.model as EventModel)}
|
||||
<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<div class="pf-c-card__title">${msg("Affected model:")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.getModelInfo(this.event.context?.model as EventModel)}
|
||||
</div>
|
||||
</div>
|
||||
${diffBody}
|
||||
</div>
|
||||
<br />
|
||||
<ak-expand>${this.renderDefaultResponse()}</ak-expand>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -305,6 +305,10 @@ A configuration error occurs, for example during the authorization of an applica
|
||||
|
||||
Logged when any model is created/updated/deleted, including the user that sent the request.
|
||||
|
||||
:::info
|
||||
Starting with authentik Enterprise 2024.1, `model_*` events also include which fields have been changed and their previous and new values.
|
||||
:::
|
||||
|
||||
### `email_sent`
|
||||
|
||||
An email has been sent. Included is the email that was sent.
|
||||
|
@ -70,6 +70,10 @@ slug: /releases/2024.1
|
||||
|
||||
It allows for authentik operators to manage several authentik installations without having to deploy additional instances.
|
||||
|
||||
- Audit log <span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
authentik instances which have a valid enterprise license installed will log changes made to models including which fields were changed with previous and new values of the fields. The values are censored if they are sensitive (for example a password hash), however a hash of the changed value will still be logged.
|
||||
|
||||
- "Pretend user exists" option for Identification stage
|
||||
|
||||
Previously the identification stage would only continue if a user matching the user identifier exists. While this was the intended functionality, this release adds an option to continue to the next stage even if no matching user was found. "Pretend" users cannot authenticate nor receive emails, and don't exist in the database. **This feature is enabled by default.**
|
||||
|
Reference in New Issue
Block a user