core: add impersonation start/end to audit log
also add impersonated user as context to other logs
This commit is contained in:
parent
e0c104ee5c
commit
fe4a0c3b44
33
passbook/audit/migrations/0002_auto_20200918_2116.py
Normal file
33
passbook/audit/migrations/0002_auto_20200918_2116.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 3.1.1 on 2020-09-18 21:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_audit", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("LOGIN", "login"),
|
||||||
|
("LOGIN_FAILED", "login_failed"),
|
||||||
|
("LOGOUT", "logout"),
|
||||||
|
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||||
|
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||||
|
("SIGN_UP", "sign_up"),
|
||||||
|
("PASSWORD_RESET", "password_reset"),
|
||||||
|
("INVITE_CREATED", "invitation_created"),
|
||||||
|
("INVITE_USED", "invitation_used"),
|
||||||
|
("IMPERSONATION_STARTED", "impersonation_started"),
|
||||||
|
("IMPERSONATION_ENDED", "impersonation_ended"),
|
||||||
|
("CUSTOM", "custom"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,15 +6,16 @@ from uuid import UUID, uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
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 django.views.debug import SafeExceptionReporterFilter
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER
|
||||||
from passbook.lib.utils.http import get_client_ip
|
from passbook.lib.utils.http import get_client_ip
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -36,6 +37,19 @@ def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
return final_dict
|
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 sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
"""clean source of all Models that would interfere with the JSONField.
|
"""clean source of all Models that would interfere with the JSONField.
|
||||||
Models are replaced with a dictionary of {
|
Models are replaced with a dictionary of {
|
||||||
|
@ -48,18 +62,7 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
final_dict[key] = sanitize_dict(value)
|
final_dict[key] = sanitize_dict(value)
|
||||||
elif isinstance(value, models.Model):
|
elif isinstance(value, models.Model):
|
||||||
model_content_type = ContentType.objects.get_for_model(value)
|
final_dict[key] = sanitize_dict(model_to_dict(value))
|
||||||
name = str(value)
|
|
||||||
if hasattr(value, "name"):
|
|
||||||
name = value.name
|
|
||||||
final_dict[key] = sanitize_dict(
|
|
||||||
{
|
|
||||||
"app": model_content_type.app_label,
|
|
||||||
"model_name": model_content_type.model,
|
|
||||||
"pk": value.pk,
|
|
||||||
"name": name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif isinstance(value, UUID):
|
elif isinstance(value, UUID):
|
||||||
final_dict[key] = value.hex
|
final_dict[key] = value.hex
|
||||||
else:
|
else:
|
||||||
|
@ -79,6 +82,8 @@ class EventAction(Enum):
|
||||||
PASSWORD_RESET = "password_reset" # noqa # nosec
|
PASSWORD_RESET = "password_reset" # noqa # nosec
|
||||||
INVITE_CREATED = "invitation_created"
|
INVITE_CREATED = "invitation_created"
|
||||||
INVITE_USED = "invitation_used"
|
INVITE_USED = "invitation_used"
|
||||||
|
IMPERSONATION_STARTED = "impersonation_started"
|
||||||
|
IMPERSONATION_ENDED = "impersonation_ended"
|
||||||
CUSTOM = "custom"
|
CUSTOM = "custom"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -140,6 +145,12 @@ class Event(models.Model):
|
||||||
self.user = request.user
|
self.user = request.user
|
||||||
if user:
|
if user:
|
||||||
self.user = user
|
self.user = user
|
||||||
|
# Check if we're currently impersonating, and add that user
|
||||||
|
if hasattr(request, "session"):
|
||||||
|
if SESSION_IMPERSONATE_ORIGINAL_USER in request.session:
|
||||||
|
self.context["on_behalf_of"] = model_to_dict(
|
||||||
|
request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
)
|
||||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||||
self.client_ip = get_client_ip(request) or "255.255.255.255"
|
self.client_ip = get_client_ip(request) or "255.255.255.255"
|
||||||
# If there's no app set, we get it from the requests too
|
# If there's no app set, we get it from the requests too
|
||||||
|
|
|
@ -5,6 +5,7 @@ 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 passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.middleware import (
|
from passbook.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
SESSION_IMPERSONATE_USER,
|
SESSION_IMPERSONATE_USER,
|
||||||
|
@ -30,7 +31,7 @@ class ImpersonateInitView(View):
|
||||||
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||||
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
||||||
|
|
||||||
# TODO Audit log entry
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request)
|
||||||
|
|
||||||
return redirect("passbook_core:overview")
|
return redirect("passbook_core:overview")
|
||||||
|
|
||||||
|
@ -50,6 +51,6 @@ class ImpersonateEndView(View):
|
||||||
del request.session[SESSION_IMPERSONATE_USER]
|
del request.session[SESSION_IMPERSONATE_USER]
|
||||||
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
# TODO: Audit log entry
|
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request)
|
||||||
|
|
||||||
return redirect("passbook_core:overview")
|
return redirect("passbook_core:overview")
|
||||||
|
|
|
@ -5846,6 +5846,8 @@ definitions:
|
||||||
- PASSWORD_RESET
|
- PASSWORD_RESET
|
||||||
- INVITE_CREATED
|
- INVITE_CREATED
|
||||||
- INVITE_USED
|
- INVITE_USED
|
||||||
|
- IMPERSONATION_STARTED
|
||||||
|
- IMPERSONATION_ENDED
|
||||||
- CUSTOM
|
- CUSTOM
|
||||||
date:
|
date:
|
||||||
title: Date
|
title: Date
|
||||||
|
|
Reference in a new issue