From 7a7b3bef2938497eb3479216ca8e148b084041d5 Mon Sep 17 00:00:00 2001 From: Edward <9326037+edwardoliveira@users.noreply.github.com> Date: Tue, 15 Oct 2019 11:58:05 -0300 Subject: [PATCH] Adiciona log para create, update, delete. (#3016) --- sapl/base/admin.py | 31 ++++++++++++++++-- sapl/base/migrations/0038_auditlog.py | 33 +++++++++++++++++++ sapl/base/models.py | 46 +++++++++++++++++++++++++++ sapl/base/receivers.py | 42 +++++++++++++++++++++++- sapl/base/signals.py | 3 ++ sapl/crud/base.py | 37 ++++++++++++++++++--- sapl/materia/forms.py | 1 + sapl/materia/views.py | 17 +++++++--- sapl/protocoloadm/forms.py | 5 +-- sapl/protocoloadm/views.py | 15 ++++++--- sapl/rules/map_rules.py | 1 + 11 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 sapl/base/migrations/0038_auditlog.py diff --git a/sapl/base/admin.py b/sapl/base/admin.py index 88633b0fb..46ab77bf3 100644 --- a/sapl/base/admin.py +++ b/sapl/base/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin -from django.core.urlresolvers import reverse from django.shortcuts import redirect from django.utils.translation import ugettext_lazy as _ from reversion.models import Revision + +from sapl.base.models import AuditLog from sapl.utils import register_all_models_in_admin register_all_models_in_admin(__name__) @@ -20,5 +21,31 @@ class RevisionAdmin(admin.ModelAdmin): self.message_user(request, _('You cannot change history.')) return redirect('admin:reversion_revision_changelist') - admin.site.register(Revision, RevisionAdmin) + + +class AuditLogAdmin(admin.ModelAdmin): + pass + + def has_add_permission(self, request): + return False + + # def has_change_permission(self, request, obj=None): + # return False + # + def has_delete_permission(self, request, obj=None): + return False + + def save_model(self, request, obj, form, change): + pass + + def delete_model(self, request, obj): + pass + + def save_related(self, request, form, formsets, change): + pass + + +# Na linha acima register_all_models_in_admin registrou AuditLog +admin.site.unregister(AuditLog) +admin.site.register(AuditLog, AuditLogAdmin) diff --git a/sapl/base/migrations/0038_auditlog.py b/sapl/base/migrations/0038_auditlog.py new file mode 100644 index 000000000..ba89599e7 --- /dev/null +++ b/sapl/base/migrations/0038_auditlog.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-10-15 13:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0037_auto_20190527_0901'), + ] + + operations = [ + migrations.CreateModel( + name='AuditLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(blank=True, db_index=True, max_length=100, verbose_name='username')), + ('operation', models.CharField(db_index=True, max_length=1, verbose_name='operation')), + ('timestamp', models.DateTimeField(db_index=True, verbose_name='timestamp')), + ('object', models.CharField(blank=True, max_length=4096, verbose_name='object')), + ('object_id', models.PositiveIntegerField(db_index=True, verbose_name='object_id')), + ('model_name', models.CharField(db_index=True, max_length=100, verbose_name='model')), + ('app_name', models.CharField(db_index=True, max_length=100, verbose_name='app')), + ], + options={ + 'verbose_name': 'AuditLog', + 'verbose_name_plural': 'AuditLogs', + 'ordering': ('-id',), + }, + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 7f2ea8d6b..1861cd3f2 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -265,3 +265,49 @@ class AutorUser(models.Model): def __str__(self): return "%s - %s" % (self.autor, self.user) + + +class AuditLog(models.Model): + + operation = ('C', 'D', 'U') + + MAX_DATA_LENGTH = 4096 # 4KB de texto + + username = models.CharField(max_length=100, + verbose_name=_('username'), + blank=True, + db_index=True) + operation = models.CharField(max_length=1, + verbose_name=_('operation'), + db_index=True) + timestamp = models.DateTimeField(verbose_name=_('timestamp'), + db_index=True) + object = models.CharField(max_length=MAX_DATA_LENGTH, + blank=True, + verbose_name=_('object')) + object_id = models.PositiveIntegerField(verbose_name=_('object_id'), + db_index=True) + model_name = models.CharField(max_length=100, verbose_name=_('model'), + db_index=True) + app_name = models.CharField(max_length=100, + verbose_name=_('app'), + db_index=True) + + class Meta: + verbose_name = _('AuditLog') + verbose_name_plural = _('AuditLogs') + ordering = ('-id',) + + def __str__(self): + return "[%s] %s %s.%s %s" % (self.timestamp, + self.operation, + self.app_name, + self.model_name, + self.username, + ) + + +def cria_models_tipo_autor(app_config=None, verbosity=2, interactive=True, + using=DEFAULT_DB_ALIAS, **kwargs): + + models = models_with_gr_for_model(Autor) \ No newline at end of file diff --git a/sapl/base/receivers.py b/sapl/base/receivers.py index a33edeee0..0cf8756da 100644 --- a/sapl/base/receivers.py +++ b/sapl/base/receivers.py @@ -1,7 +1,13 @@ +import logging + +from django.core import serializers from django.db.models.signals import post_delete from django.dispatch import receiver +from django.utils import timezone -from sapl.base.signals import tramitacao_signal +from sapl.base.email_utils import do_envia_email_tramitacao +from sapl.base.models import AuditLog +from sapl.base.signals import tramitacao_signal, post_delete_signal, post_save_signal from sapl.materia.models import Tramitacao from sapl.protocoloadm.models import TramitacaoAdministrativo from sapl.utils import get_base_url @@ -39,3 +45,37 @@ def status_tramitacao_materia(sender, instance, **kwargs): documento = instance.documento documento.tramitacao = True documento.save() + + +@receiver(post_delete_signal) +@receiver(post_save_signal) +def audit_log(sender, **kwargs): + logger = logging.getLogger(__name__) + + instance = kwargs.get('instance') + operation = kwargs.get('operation') + user = kwargs.get('request').user + model_name = instance.__class__.__name__ + app_name = instance._meta.app_label + object_id = instance.id + data = serializers.serialize('json', [instance]) + + if len(data) > AuditLog.MAX_DATA_LENGTH: + data = data[:AuditLog.MAX_DATA_LENGTH] + + if user: + username = user.username + else: + username = '' + + try: + AuditLog.objects.create(username=username, + operation=operation, + model_name=model_name, + app_name=app_name, + timestamp=timezone.now(), + object_id=object_id, + object=data) + except Exception as e: + logger.error('Error saving auditing log object') + logger.error(e) diff --git a/sapl/base/signals.py b/sapl/base/signals.py index b2cda2028..a2caf67df 100644 --- a/sapl/base/signals.py +++ b/sapl/base/signals.py @@ -46,3 +46,6 @@ def cria_models_tipo_autor(app_config=None, verbosity=2, interactive=True, post_migrate.connect(receiver=cria_models_tipo_autor) +post_delete_signal = django.dispatch.Signal(providing_args=['instance', 'request']) + +post_save_signal = django.dispatch.Signal(providing_args=['instance', 'operation', 'request']) diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 18f3d5c29..e0fe78db0 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -16,7 +16,6 @@ from django.http.response import Http404 from django.shortcuts import redirect from django.utils.decorators import classonlymethod from django.utils.encoding import force_text -from django.utils.functional import cached_property from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ from django.views.generic import (CreateView, DeleteView, DetailView, ListView, @@ -24,6 +23,7 @@ from django.views.generic import (CreateView, DeleteView, DetailView, ListView, from django.views.generic.base import ContextMixin from django.views.generic.list import MultipleObjectMixin +from sapl.base.signals import post_delete_signal, post_save_signal from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import SaplFormHelper from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, @@ -618,8 +618,37 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView): return queryset +class AuditLogMixin(object): + + def delete(self, request, *args, **kwargs): + # Classe deve implementar um get_object(), i.e., deve ser uma View + deleted_object = self.get_object() + try: + return super(AuditLogMixin, self).delete(request, args, kwargs) + finally: + post_delete_signal.send(sender=None, + instance=deleted_object, + operation='D', + request=self.request) + + # SAVE/UPDATE method + def form_valid(self, form): + try: + if not form.instance.pk: + operation = 'C' + else: + operation = 'U' + return super(AuditLogMixin, self).form_valid(form) + finally: + post_save_signal.send(sender=None, + instance=form.instance, + operation=operation, + request=self.request + ) + + class CrudCreateView(PermissionRequiredContainerCrudMixin, - FormMessagesMixin, CreateView): + FormMessagesMixin, AuditLogMixin, CreateView): permission_required = (RP_ADD, ) logger = logging.getLogger(__name__) @@ -837,7 +866,7 @@ class CrudDetailView(PermissionRequiredContainerCrudMixin, class CrudUpdateView(PermissionRequiredContainerCrudMixin, - FormMessagesMixin, UpdateView): + FormMessagesMixin, AuditLogMixin, UpdateView): permission_required = (RP_CHANGE, ) logger = logging.getLogger(__name__) @@ -868,7 +897,7 @@ class CrudUpdateView(PermissionRequiredContainerCrudMixin, class CrudDeleteView(PermissionRequiredContainerCrudMixin, - FormMessagesMixin, DeleteView): + FormMessagesMixin, AuditLogMixin, DeleteView): permission_required = (RP_DELETE, ) logger = logging.getLogger(__name__) diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 4cf2d5314..e04addc5d 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -24,6 +24,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from sapl.base.models import AppConfig, Autor, TipoAutor +from sapl.base.signals import post_save_signal from sapl.comissoes.models import Comissao, Composicao, Participacao from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, STATUS_TA_PRIVATE) diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 807dae9da..89fab39af 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -33,13 +33,13 @@ from django_filters.views import FilterView from sapl.base.email_utils import do_envia_email_confirmacao from sapl.base.models import Autor, CasaLegislativa, AppConfig as BaseAppConfig -from sapl.base.signals import tramitacao_signal +from sapl.base.signals import tramitacao_signal, post_delete_signal, post_save_signal from sapl.comissoes.models import Comissao, Participacao, Composicao from sapl.compilacao.models import STATUS_TA_IMMUTABLE_RESTRICT, STATUS_TA_PRIVATE from sapl.compilacao.views import IntegracaoTaView from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, SaplFormLayout from sapl.crud.base import (Crud, CrudAux, make_pagination, MasterDetailCrud, - PermissionRequiredForAppCrudMixin, RP_DETAIL, RP_LIST) + PermissionRequiredForAppCrudMixin, RP_DETAIL, RP_LIST,) from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm, ConfirmarProposicaoForm, DevolverProposicaoForm, DespachoInicialCreateForm, LegislacaoCitadaForm, @@ -1390,7 +1390,7 @@ class TramitacaoCrud(MasterDetailCrud): messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(url) else: - tramitacoes_deletar = [tramitacao.id] + tramitacoes_deletar = [tramitacao] if materia.tramitacao_set.count() == 0: materia.em_tramitacao = False materia.save() @@ -1401,11 +1401,18 @@ class TramitacaoCrud(MasterDetailCrud): for ma in mat_anexadas: tram_anexada = ma.tramitacao_set.last() if compara_tramitacoes_mat(tram_anexada, tramitacao): - tramitacoes_deletar.append(tram_anexada.id) + tramitacoes_deletar.append(tram_anexada) if ma.tramitacao_set.count() == 0: ma.em_tramitacao = False ma.save() - Tramitacao.objects.filter(id__in=tramitacoes_deletar).delete() + Tramitacao.objects.filter(id__in=[t.id for t in tramitacoes_deletar]).delete() + + # TODO: otimizar para passar a lista de matérias + for tramitacao in tramitacoes_deletar: + post_delete_signal.send(sender=None, + instance=tramitacao, + operation='C', + request=self.request) return HttpResponseRedirect(url) diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index cae67d2ab..f44d3321e 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -2,6 +2,8 @@ import logging from crispy_forms.bootstrap import InlineRadios, Alert, FormActions + +from sapl.base.signals import post_save_signal from sapl.crispy_layout_mixin import SaplFormHelper from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout, Div, Submit from django import forms @@ -1563,7 +1565,6 @@ class TramitacaoEmLoteAdmForm(ModelForm): ) ) - def clean(self): cleaned_data = super(TramitacaoEmLoteAdmForm, self).clean() @@ -1655,7 +1656,7 @@ class TramitacaoEmLoteAdmForm(ModelForm): user=tramitacao.user, ip=tramitacao.ip )) - TramitacaoAdministrativo.objects.bulk_create(lista_tramitacao) + TramitacaoAdministrativo.objects.bulk_create(lista_tramitacao) return tramitacao diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index 830f6bf76..40902ce17 100755 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -26,7 +26,7 @@ from django_filters.views import FilterView import sapl from sapl.base.email_utils import do_envia_email_confirmacao from sapl.base.models import Autor, CasaLegislativa, AppConfig -from sapl.base.signals import tramitacao_signal +from sapl.base.signals import tramitacao_signal, post_delete_signal from sapl.comissoes.models import Comissao from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, make_pagination, RP_LIST, RP_DETAIL) @@ -1268,7 +1268,7 @@ class TramitacaoAdmCrud(MasterDetailCrud): messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(url) else: - tramitacoes_deletar = [tramitacao.id] + tramitacoes_deletar = [tramitacao] if documento.tramitacaoadministrativo_set.count() == 0: documento.tramitacao = False documento.save() @@ -1278,12 +1278,19 @@ class TramitacaoAdmCrud(MasterDetailCrud): for da in docs_anexados: tram_anexada = da.tramitacaoadministrativo_set.last() if compara_tramitacoes_doc(tram_anexada, tramitacao): - tramitacoes_deletar.append(tram_anexada.id) + tramitacoes_deletar.append(tram_anexada) if da.tramitacaoadministrativo_set.count() == 0: da.tramitacao = False da.save() TramitacaoAdministrativo.objects.filter( - id__in=tramitacoes_deletar).delete() + id__in=[t.id for t in tramitacoes_deletar]).delete() + + # TODO: otimizar para passar a lista de matérias + for tramitacao in tramitacoes_deletar: + post_delete_signal.send(sender=None, + instance=tramitacao, + operation='C', + request=self.request) return HttpResponseRedirect(url) diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index 4e33d546f..5d5028b33 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -236,6 +236,7 @@ rules_group_geral = { (base.TipoAutor, __base__, __perms_publicas__), (base.Autor, __base__, __perms_publicas__), (base.AutorUser, __base__, __perms_publicas__), + (base.AuditLog, __base__, set()), (protocoloadm.StatusTramitacaoAdministrativo, __base__, set()), (protocoloadm.TipoDocumentoAdministrativo, __base__, set()),