ISO-27001-Risk-Management/risks/admin.py

226 lines
8.8 KiB
Python
Raw Normal View History

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from .models import (
Control,
Incident,
Notification,
NotificationPreference,
NotificationRule,
Risk,
ResidualRisk,
User,
)
# ---------------------------------------------------------------------------
# Global Admin Settings
# ---------------------------------------------------------------------------
admin.site.site_header = _("Administration")
admin.site.site_title = _("Admin")
admin.site.index_title = _("Administration")
# ---- Inlines ----
class NotificationPreferenceInline(admin.StackedInline):
"""Preferences inline for notifications on User model"""
model = NotificationPreference
can_delete = False
extra = 0
fieldsets = (
(_("Risks"), {"fields": ("risk_created", "risk_updated", "risk_deleted")}),
(_("Controls"), {"fields": ("control_created", "control_updated", "control_deleted")}),
(_("Residual risks"), {"fields": ("residual_created", "residual_updated", "residual_deleted")}),
(_("Reviews"), {"fields": ("review_required", "review_completed")}),
(_("Incidents"), {"fields": ("incident_created", "incident_updated", "incident_deleted")}),
(_("Users"), {"fields": ("user_created", "user_deleted")}),
)
class ResidualRiskInline(admin.StackedInline):
"""Inline editor for ResidualRisk (one-to-one with Risk)"""
model = ResidualRisk
extra = 0
can_delete = False
readonly_fields = ("score", "level", "review_required")
fields = ("likelihood", "impact", "score", "level", "review_required")
class ControlRisksInline(admin.TabularInline):
"""M2M relation between Risk and Control"""
model = Control.risks.through
fk_name = "risk"
extra = 1
autocomplete_fields = ("control",)
class NotificationInline(admin.TabularInline):
"""Inline display of notifications on User model"""
model = Notification
fields = ("created_at", "message", "read", "sent")
readonly_fields = ("created_at", "message")
extra = 0
ordering = ("-created_at",)
# ---- Shared Mixins ----
class ChangedByMixin:
"""Automatically track user who created/changed/deleted"""
def save_model(self, request, obj, form, change):
obj._changed_by = request.user
super().save_model(request, obj, form, change)
def delete_model(self, request, obj):
obj._changed_by = request.user
super().delete_model(request, obj)
class RedirectOnSaveMixin:
"""Redirect to detail view instead of staying in admin"""
redirect_url_name = None
def response_add(self, request, obj, post_url_continue=None):
return HttpResponseRedirect(reverse(self.redirect_url_name, args=[obj.pk]))
def response_change(self, request, obj):
return HttpResponseRedirect(reverse(self.redirect_url_name, args=[obj.pk]))
# ---------------------------------------------------------------------------
# Risk
# ---------------------------------------------------------------------------
@admin.register(Risk)
class RiskAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_risk"
list_display = ("title", "owner_name", "status", "score", "level", "likelihood", "impact", "follow_up")
list_filter = ("status", "level", "likelihood", "impact", "owner")
search_fields = ("title", "asset", "process", "category")
inlines = [ResidualRiskInline, ControlRisksInline]
def owner_name(self, obj):
if not obj.owner:
return "-"
return obj.owner.get_full_name() or obj.owner.username
# ---------------------------------------------------------------------------
# Residual Risk
# ---------------------------------------------------------------------------
@admin.register(ResidualRisk)
class ResidualRiskAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_risk"
list_display = ("risk", "score", "level", "likelihood", "impact", "review_required")
list_filter = ("level", "likelihood", "impact", "review_required")
# ---------------------------------------------------------------------------
# Control
# ---------------------------------------------------------------------------
@admin.register(Control)
class ControlAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_control"
list_display = ("title", "status", "due_date", "responsible")
list_filter = ("status", "due_date")
autocomplete_fields = ("risks", "responsible")
search_fields = ("title", "description")
# ---------------------------------------------------------------------------
# Incident
# ---------------------------------------------------------------------------
@admin.register(Incident)
class IncidentAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_incident"
list_display = ("title", "date_reported", "reported_by", "status")
list_filter = ("status", "date_reported", "reported_by")
search_fields = ("title", "description")
autocomplete_fields = ("related_risks",)
filter_horizontal = ("related_risks",)
# ---------------------------------------------------------------------------
# Notification
# ---------------------------------------------------------------------------
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
date_hierarchy = "created_at"
list_display = ("id", "created_at", "user_display", "short_message", "read", "sent")
list_display_links = ("id", "short_message")
list_filter = ("read", "sent", "created_at")
search_fields = ("message", "user__username", "user__first_name", "user__last_name", "user__email")
list_select_related = ("user",)
list_editable = ("read", "sent")
ordering = ("-created_at",)
autocomplete_fields = ("user",)
actions = ["mark_as_read", "mark_as_unread", "mark_as_sent", "mark_as_unsent"]
@admin.display(description=_("User"))
def user_display(self, obj):
return obj.user.get_full_name() if obj.user else ""
@admin.display(description=_("Message"))
def short_message(self, obj):
msg = obj.message or ""
return (msg[:80] + "") if len(msg) > 80 else msg
# Bulk actions
@admin.action(description=_("Mark selected as read"))
def mark_as_read(self, request, queryset):
n = queryset.update(read=True)
self.message_user(request, _("%(n)d notifications marked as read.") % {"n": n})
@admin.action(description=_("Mark selected as unread"))
def mark_as_unread(self, request, queryset):
n = queryset.update(read=False)
self.message_user(request, _("%(n)d notifications marked as unread.") % {"n": n})
@admin.action(description=_("Mark selected as sent"))
def mark_as_sent(self, request, queryset):
n = queryset.update(sent=True)
self.message_user(request, _("%(n)d notifications marked as sent.") % {"n": n})
@admin.action(description=_("Mark selected as unsent"))
def mark_as_unsent(self, request, queryset):
n = queryset.update(sent=False)
self.message_user(request, _("%(n)d notifications marked as unsent.") % {"n": n})
# ---- Notification Rule ----
@admin.register(NotificationRule)
class NotificationRuleAdmin(admin.ModelAdmin):
list_display = ("kind", "enabled_in_app", "enabled_email", "to_owner", "to_staff", "short_extras")
list_editable = ("enabled_in_app", "enabled_email", "to_owner", "to_staff")
list_filter = ("enabled_in_app", "enabled_email", "to_owner", "to_staff")
search_fields = ("kind", "extra_recipients")
ordering = ("kind",)
@admin.display(description=_("Extra recipients"))
def short_extras(self, obj):
txt = (obj.extra_recipients or "").replace("\n", ", ")
return (txt[:50] + "") if len(txt) > 50 else txt
# ---------------------------------------------------------------------------
# User (extension)
# ---------------------------------------------------------------------------
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = BaseUserAdmin.fieldsets + (
(_("SSO Information"), {"fields": ("is_sso_user",)}),
)
list_display = (
"username", "email", "is_staff", "is_superuser", "is_sso_user",
"owned_risks_count", "responsible_controls_count"
)
inlines = [NotificationInline, NotificationPreferenceInline]
def owned_risks_count(self, obj):
return obj.risks_owned.count()
owned_risks_count.short_description = _("Risks Owned")
def responsible_controls_count(self, obj):
return obj.controls_responsible.count()
responsible_controls_count.short_description = _("Controls Responsible")