diff --git a/sapl/base/migrations/0065_add_permite_remover_assinatura_e_permissoes.py b/sapl/base/migrations/0065_add_permite_remover_assinatura_e_permissoes.py new file mode 100644 index 000000000..465a2d8a2 --- /dev/null +++ b/sapl/base/migrations/0065_add_permite_remover_assinatura_e_permissoes.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2026-05-14 11:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0064_appconfig_revisao_setor_legislativo'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='permite_remover_assinatura', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, help_text='Quando habilitado, superusuários e usuários com a permissão "Pode remover assinatura digital" poderão remover assinaturas de matérias e documentos acessórios para permitir edições.', verbose_name='Permitir remoção de assinatura digital?'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 9edc27862..cadddb772 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -238,6 +238,16 @@ class AppConfig(models.Model): verbose_name=_('Nome utilizado na assinatura digital'), choices=TIPO_NOME_ASSINATURA, default='P') + permite_remover_assinatura = models.BooleanField( + verbose_name=_('Permitir remoção de assinatura digital?'), + choices=YES_NO_CHOICES, + default=False, + help_text=_( + 'Quando habilitado, superusuários e usuários com a permissão ' + '"Pode remover assinatura digital" poderão remover assinaturas ' + 'de matérias e documentos acessórios para permitir edições.' + ) + ) # MÓDULO PAINEL cronometro_discurso = models.DurationField( verbose_name=_('Cronômetro do Discurso'), diff --git a/sapl/materia/migrations/0096_add_permite_remover_assinatura_e_permissoes.py b/sapl/materia/migrations/0096_add_permite_remover_assinatura_e_permissoes.py new file mode 100644 index 000000000..248e8a29b --- /dev/null +++ b/sapl/materia/migrations/0096_add_permite_remover_assinatura_e_permissoes.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.28 on 2026-05-14 11:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0095_add_autoria_proposicao'), + ] + + operations = [ + migrations.AlterModelOptions( + name='documentoacessorio', + options={'ordering': ('data', 'id'), 'permissions': (('can_remove_assinatura_doc', 'Pode remover assinatura digital de documento acessório'),), 'verbose_name': 'Documento Acessório', 'verbose_name_plural': 'Documentos Acessórios'}, + ), + migrations.AlterModelOptions( + name='materialegislativa', + options={'ordering': ['-ano', 'tipo', 'numero'], 'permissions': (('can_access_impressos', 'Can access impressos'), ('can_remove_assinatura', 'Pode remover assinatura digital')), 'verbose_name': 'Matéria Legislativa', 'verbose_name_plural': 'Matérias Legislativas'}, + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 7048117fc..0d490ded0 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -345,7 +345,10 @@ class MateriaLegislativa(models.Model): verbose_name_plural = _('Matérias Legislativas') unique_together = (("tipo", "numero", "ano"),) ordering = ['-ano', 'tipo', 'numero'] - permissions = (("can_access_impressos", "Can access impressos"),) + permissions = ( + ("can_access_impressos", "Can access impressos"), + ("can_remove_assinatura", "Pode remover assinatura digital"), + ) def __str__(self): return _('%(tipo)s nº %(numero)s de %(ano)s') % { @@ -634,6 +637,9 @@ class DocumentoAcessorio(models.Model): verbose_name = _('Documento Acessório') verbose_name_plural = _('Documentos Acessórios') ordering = ('data', 'id') + permissions = ( + ("can_remove_assinatura_doc", "Pode remover assinatura digital de documento acessório"), + ) def __str__(self): return _('%(tipo)s - %(nome)s de %(data)s por %(autor)s') % { diff --git a/sapl/materia/views.py b/sapl/materia/views.py index e8007d655..bce2dc4a4 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -1996,13 +1996,16 @@ class DocumentoAcessorioCrud(MasterDetailCrud): ) # Normaliza assinatura_info para lista (backward-compatible) - from sapl.materia.views_assinatura import _normalizar_assinatura_info + from sapl.materia.views_assinatura import _normalizar_assinatura_info, _pode_remover_assinatura assinaturas = _normalizar_assinatura_info(self.object.assinatura_info) context['assinaturas'] = assinaturas context['ja_assinou'] = any( a.get('signed_by') == u.username for a in assinaturas ) if u.is_authenticated else False + context['pode_remover_assinatura'] = ( + u.is_authenticated and _pode_remover_assinatura(u) + ) return context @@ -2529,13 +2532,17 @@ class MateriaLegislativaCrud(Crud): ) # Normaliza assinatura_info para lista (backward-compatible) - from sapl.materia.views_assinatura import _normalizar_assinatura_info + from sapl.materia.views_assinatura import _normalizar_assinatura_info, _pode_remover_assinatura assinaturas = _normalizar_assinatura_info(self.object.assinatura_info) context['assinaturas'] = assinaturas context['ja_assinou'] = any( a.get('signed_by') == self.request.user.username for a in assinaturas ) if self.request.user.is_authenticated else False + context['pode_remover_assinatura'] = ( + self.request.user.is_authenticated and + _pode_remover_assinatura(self.request.user) + ) return context diff --git a/sapl/materia/views_assinatura.py b/sapl/materia/views_assinatura.py index f626e88c8..0a743cf57 100644 --- a/sapl/materia/views_assinatura.py +++ b/sapl/materia/views_assinatura.py @@ -28,6 +28,23 @@ from sapl.utils import build_onlyoffice_url logger = logging.getLogger(__name__) +def _pode_remover_assinatura(user): + """ + Verifica se o usuário tem permissão para remover assinaturas digitais. + Regras (qualquer uma é suficiente): + 1. É superusuário, OU + 2. A funcionalidade está habilitada em AppConfig E o usuário possui a + permissão granular 'materia.can_remove_assinatura'. + """ + if user.is_superuser: + return True + from sapl.base.models import AppConfig + permite = AppConfig.attr('permite_remover_assinatura') + if permite and user.has_perm('materia.can_remove_assinatura'): + return True + return False + + def _normalizar_assinatura_info(info): """Converte assinatura_info legado (dict) para lista de dicts.""" if info is None: @@ -1215,12 +1232,13 @@ def materia_verificar_assinatura(request, pk): def materia_remover_assinatura(request, pk): """ Remove a assinatura digital da matéria. - Apenas superusuários podem executar esta ação. + Requer: superusuário OU funcionalidade habilitada em AppConfig + E permissão 'materia.can_remove_assinatura'. """ - if not request.user.is_superuser: + if not _pode_remover_assinatura(request.user): return JsonResponse({ 'success': False, - 'error': 'Apenas administradores podem remover assinaturas.' + 'error': 'Você não tem permissão para remover assinaturas digitais.' }, status=403) materia = get_object_or_404(MateriaLegislativa, pk=pk) @@ -1641,12 +1659,16 @@ def docacessorio_verificar_assinatura(request, pk): def docacessorio_remover_assinatura(request, pk): """ Remove a assinatura digital do documento acessório. - Apenas superusuários podem executar esta ação. + Requer: superusuário OU funcionalidade habilitada em AppConfig + E permissão 'materia.can_remove_assinatura_doc'. """ - if not request.user.is_superuser: + if not (request.user.is_superuser or ( + AppConfig.attr('permite_remover_assinatura') and + request.user.has_perm('materia.can_remove_assinatura_doc') + )): return JsonResponse({ 'success': False, - 'error': 'Apenas administradores podem remover assinaturas.' + 'error': 'Você não tem permissão para remover assinaturas digitais.' }, status=403) docacessorio = get_object_or_404(DocumentoAcessorio, pk=pk) diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index ebf1e43eb..b43d0428d 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -48,6 +48,7 @@ AppConfig: {% trans 'Módulo Assinatura Digital' %}: - assinatura_nome + - permite_remover_assinatura {% trans 'Módulo Painel' %}: - cronometro_discurso cronometro_aparte diff --git a/sapl/templates/materia/documentoacessorio_detail.html b/sapl/templates/materia/documentoacessorio_detail.html index 12a41602d..90c59bff0 100644 --- a/sapl/templates/materia/documentoacessorio_detail.html +++ b/sapl/templates/materia/documentoacessorio_detail.html @@ -78,6 +78,13 @@ {% trans "Verificar Autenticidade" %} {% endif %} + {% if pode_remover_assinatura %} + + {% endif %} @@ -98,4 +105,7 @@ }); {% endif %} +{% if pode_remover_assinatura and object.pdf_assinado %} + {% include "materia/remover_assinatura_modal.html" %} +{% endif %} {% endblock extra_js %} diff --git a/sapl/templates/materia/materialegislativa_detail.html b/sapl/templates/materia/materialegislativa_detail.html index 210bd583a..e9555dbd7 100644 --- a/sapl/templates/materia/materialegislativa_detail.html +++ b/sapl/templates/materia/materialegislativa_detail.html @@ -230,6 +230,13 @@ {% trans "Verificar Autenticidade" %} {% endif %} + {% if pode_remover_assinatura %} + + {% endif %} @@ -250,4 +257,7 @@ }); {% endif %} +{% if pode_remover_assinatura and object.pdf_assinado %} + {% include "materia/remover_assinatura_modal.html" %} +{% endif %} {% endblock extra_js %} diff --git a/sapl/templates/materia/remover_assinatura_modal.html b/sapl/templates/materia/remover_assinatura_modal.html new file mode 100644 index 000000000..d60608781 --- /dev/null +++ b/sapl/templates/materia/remover_assinatura_modal.html @@ -0,0 +1,135 @@ +{% load i18n %} + + + + +