Browse Source

Ajusta regras de acesso a arquivos de documentos e proposições

Documentos administrativos e acessórios ficam ocultos ao público quando a
configuração é restritiva ou o documento é restrito. O texto original da
proposição nunca é público. Arquivos de sessões plenárias são públicos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pull/3865/head
Edward Ribeiro 3 weeks ago
parent
commit
a7b8802dc2
  1. 30
      sapl/base/media.py
  2. 27
      sapl/base/tests/test_media.py
  3. 12
      sapl/materia/models.py
  4. 10
      sapl/materia/views.py
  5. 7
      sapl/protocoloadm/models.py
  6. 4
      sapl/protocoloadm/tests/test_docadm_acesso.py
  7. 17
      sapl/sessao/tests/test_sessao_oculta_api.py

30
sapl/base/media.py

@ -6,11 +6,10 @@ from urllib.parse import quote
from django.conf import settings from django.conf import settings
from django.http import FileResponse, Http404, HttpResponse from django.http import FileResponse, Http404, HttpResponse
from sapl.materia.models import Proposicao from sapl.materia.models import Proposicao, pode_ver_proposicao
from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo,
DocumentoAdministrativo, DocumentoAdministrativo,
pode_ver_documento_adm) pode_ver_documento_adm)
from sapl.sessao.models import SessaoPlenaria, restringe_sessoes_visiveis
from sapl.utils import EXTENSOES_ATIVAS, tipo_ativo from sapl.utils import EXTENSOES_ATIVAS, tipo_ativo
X_ACCEL_PREFIX = '/media-interna/' X_ACCEL_PREFIX = '/media-interna/'
@ -21,19 +20,6 @@ PROTEGIDO_RE = re.compile(
r'^sapl/[^/]+/(?:documentoadministrativo|' r'^sapl/[^/]+/(?:documentoadministrativo|'
r'documentoacessorioadministrativo|proposicao)/') r'documentoacessorioadministrativo|proposicao)/')
def _pode_ver_proposicao(user, proposicao):
if proposicao.data_recebimento:
return True
if not user.is_authenticated:
return False
if proposicao.autor and proposicao.autor.operadores.filter(
id=user.id).exists():
return True
return bool(proposicao.data_envio) and user.has_module_perms(
'protocoloadm')
RESOLVEDORES = { RESOLVEDORES = {
'documentoadministrativo': ( 'documentoadministrativo': (
DocumentoAdministrativo, 'texto_integral', pode_ver_documento_adm), DocumentoAdministrativo, 'texto_integral', pode_ver_documento_adm),
@ -41,7 +27,7 @@ RESOLVEDORES = {
DocumentoAcessorioAdministrativo, 'arquivo', DocumentoAcessorioAdministrativo, 'arquivo',
lambda user, obj: pode_ver_documento_adm(user, obj.documento)), lambda user, obj: pode_ver_documento_adm(user, obj.documento)),
'proposicao': ( 'proposicao': (
Proposicao, 'texto_original', _pode_ver_proposicao), Proposicao, 'texto_original', pode_ver_proposicao),
} }
@ -62,14 +48,6 @@ def _nome_protegido(request, path):
return arquivo.name[2:] if arquivo.name.startswith('./') else arquivo.name return arquivo.name[2:] if arquivo.name.startswith('./') else arquivo.name
def _sessao_oculta(request, partes):
if request.user.is_authenticated or not partes[3].isdigit():
return False
qs = SessaoPlenaria.objects.filter(pk=partes[3])
return qs.exists() and not restringe_sessoes_visiveis(
qs, request.user).exists()
def media(request, path): def media(request, path):
partes = path.split('/') partes = path.split('/')
if any(p in ('', '.', '..') for p in partes) or '\x00' in path: if any(p in ('', '.', '..') for p in partes) or '\x00' in path:
@ -78,10 +56,6 @@ def media(request, path):
protegido = path.startswith('sapl/private/') or bool( protegido = path.startswith('sapl/private/') or bool(
PROTEGIDO_RE.match(path)) PROTEGIDO_RE.match(path))
nome = _nome_protegido(request, path) if protegido else path nome = _nome_protegido(request, path) if protegido else path
if path.startswith('sapl/public/sessaoplenaria/') and len(partes) > 4 \
and _sessao_oculta(request, partes):
raise Http404
nome_arquivo = os.path.basename(nome) nome_arquivo = os.path.basename(nome)
content_type = (mimetypes.guess_type(nome_arquivo)[0] or content_type = (mimetypes.guess_type(nome_arquivo)[0] or

27
sapl/base/tests/test_media.py

@ -71,8 +71,9 @@ def _assert_servido(response, path):
('O', True, None, False), ('O', True, None, False),
('O', True, 'logado', True), ('O', True, 'logado', True),
('R', False, None, False), ('R', False, None, False),
('R', False, 'logado', False), ('R', False, 'logado', True),
('R', True, 'com_permissao', True), ('R', True, None, False),
('R', True, 'logado', True),
]) ])
def test_documento_administrativo(client, comportamento, restrito, perfil, def test_documento_administrativo(client, comportamento, restrito, perfil,
pode_ver): pode_ver):
@ -81,9 +82,6 @@ def test_documento_administrativo(client, comportamento, restrito, perfil,
texto_integral=DOC_PATH) texto_integral=DOC_PATH)
if perfil == 'logado': if perfil == 'logado':
_login(client, 'logado') _login(client, 'logado')
elif perfil == 'com_permissao':
_login(client, 'perm',
'protocoloadm.detail_documentoadministrativo')
response = _get(client, DOC_PATH) response = _get(client, DOC_PATH)
@ -180,7 +178,10 @@ def _proposicao(enviada=False, recebida=False):
('enviada', 'protocolo', True), ('enviada', 'protocolo', True),
('enviada', 'outro', False), ('enviada', 'outro', False),
('enviada', None, False), ('enviada', None, False),
('recebida', None, True), ('recebida', None, False),
('recebida', 'outro', False),
('recebida', 'protocolo', True),
('recebida', 'operador', True),
]) ])
def test_proposicao(client, estado, perfil, pode_ver): def test_proposicao(client, estado, perfil, pode_ver):
operador = _proposicao(enviada=estado == 'enviada', operador = _proposicao(enviada=estado == 'enviada',
@ -276,3 +277,17 @@ def test_senha_fraca_nao_redireciona_media(client):
assert response.status_code == 200 assert response.status_code == 200
assert response.has_header('X-Accel-Redirect') assert response.has_header('X-Accel-Redirect')
@pytest.mark.django_db(transaction=False)
@pytest.mark.parametrize('perfil', [None, 'outro'])
def test_proposicao_texto_recebida_nao_e_publica(client, perfil):
_proposicao(recebida=True)
if perfil:
_login(client, perfil)
pk = Proposicao.objects.get(texto_original=PROPOSICAO_PATH).pk
response = client.get('/proposicao/texto/%s' % pk)
assert response.status_code == 302
assert response['Location'] == '/proposicao/%s' % pk

12
sapl/materia/models.py

@ -1154,6 +1154,18 @@ class Proposicao(models.Model):
update_fields=update_fields) update_fields=update_fields)
def pode_ver_proposicao(user, proposicao):
"""Texto original da proposição: nunca público. Operadores do autor veem
sempre; o protocolo vê depois do envio."""
if not user.is_authenticated:
return False
if proposicao.autor and proposicao.autor.operadores.filter(
id=user.id).exists():
return True
return bool(proposicao.data_envio) and user.has_module_perms(
'protocoloadm')
class HistoricoProposicao(models.Model): class HistoricoProposicao(models.Model):
STATUS_PROPOSICAO = Choices(('E', 'ENVIADA', _('Enviada')), STATUS_PROPOSICAO = Choices(('E', 'ENVIADA', _('Enviada')),
('R', 'RECEBIDA', _('Recebida')), ('R', 'RECEBIDA', _('Recebida')),

10
sapl/materia/views.py

@ -74,7 +74,7 @@ from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
ReceberProposicaoForm, RelatoriaForm, ReceberProposicaoForm, RelatoriaForm,
TramitacaoEmLoteFilterSet, TramitacaoEmLoteForm, TramitacaoEmLoteFilterSet, TramitacaoEmLoteForm,
UnidadeTramitacaoForm, StatusTramitacaoFilterSet) UnidadeTramitacaoForm, StatusTramitacaoFilterSet)
from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria, DespachoInicial, from .models import (pode_ver_proposicao, AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria, DespachoInicial,
DocumentoAcessorio, MateriaAssunto, MateriaLegislativa, Numeracao, Orgao, DocumentoAcessorio, MateriaAssunto, MateriaLegislativa, Numeracao, Orgao,
Origem, Proposicao, RegimeTramitacao, Relatoria, StatusTramitacao, Origem, Proposicao, RegimeTramitacao, Relatoria, StatusTramitacao,
TipoDocumento, TipoFimRelatoria, TipoMateriaLegislativa, TipoProposicao, TipoDocumento, TipoFimRelatoria, TipoMateriaLegislativa, TipoProposicao,
@ -106,14 +106,10 @@ def proposicao_texto(request, pk):
username = request.user.username username = request.user.username
logger.debug('user=' + username + logger.debug('user=' + username +
'. Tentando obter objeto Proposicao com pk = {}.'.format(pk)) '. Tentando obter objeto Proposicao com pk = {}.'.format(pk))
proposicao = Proposicao.objects.get(pk=pk) proposicao = get_object_or_404(Proposicao, pk=pk)
if proposicao.texto_original: if proposicao.texto_original:
if (not proposicao.data_recebimento and if not pode_ver_proposicao(request.user, proposicao):
not proposicao.autor.operadores.filter(
id=request.user.id
).exists()
):
logger.error("user=" + username + ". Usuário ({}) não tem permissão para acessar o texto original." logger.error("user=" + username + ". Usuário ({}) não tem permissão para acessar o texto original."
.format(request.user.id)) .format(request.user.id))
messages.error(request, _( messages.error(request, _(

7
sapl/protocoloadm/models.py

@ -343,9 +343,10 @@ class DocumentoAdministrativo(models.Model):
def pode_ver_documento_adm(user, documento): def pode_ver_documento_adm(user, documento):
if SaplAppConfig.attr('documentos_administrativos') == DOC_ADM_OSTENSIVO: if user.is_authenticated:
return not (documento.restrito and user.is_anonymous) return True
return user.has_perm('protocoloadm.detail_documentoadministrativo') return not documento.restrito and SaplAppConfig.attr(
'documentos_administrativos') == DOC_ADM_OSTENSIVO
class DocumentoAcessorioAdministrativo(models.Model): class DocumentoAcessorioAdministrativo(models.Model):

4
sapl/protocoloadm/tests/test_docadm_acesso.py

@ -62,10 +62,10 @@ ACESSO = [
('O', True, 'sem_permissao', True), ('O', True, 'sem_permissao', True),
('O', True, 'com_permissao', True), ('O', True, 'com_permissao', True),
('R', False, 'anonimo', False), ('R', False, 'anonimo', False),
('R', False, 'sem_permissao', False), ('R', False, 'sem_permissao', True),
('R', False, 'com_permissao', True), ('R', False, 'com_permissao', True),
('R', True, 'anonimo', False), ('R', True, 'anonimo', False),
('R', True, 'sem_permissao', False), ('R', True, 'sem_permissao', True),
('R', True, 'com_permissao', True), ('R', True, 'com_permissao', True),
] ]

17
sapl/sessao/tests/test_sessao_oculta_api.py

@ -70,16 +70,9 @@ def test_api_mostra_sessao_previa_ao_usuario_autenticado(client, sessoes):
@pytest.mark.django_db(transaction=False) @pytest.mark.django_db(transaction=False)
def test_media_de_sessao_previa_oculta_do_anonimo(client, settings, sessoes): def test_media_de_sessao_e_publica(client, settings, sessoes):
settings.DEBUG = False settings.DEBUG = False
previa, iniciada = sessoes for sessao in sessoes:
response = client.get(
def get(sessao): '/media/sapl/public/sessaoplenaria/%s/pauta/p.pdf' % sessao.pk)
return client.get('/media/sapl/public/sessaoplenaria/%s/pauta/p.pdf' assert response.status_code == 200
% sessao.pk)
assert get(previa).status_code == 404
assert get(iniciada).status_code == 200
client.force_login(baker.make(get_user_model()))
assert get(previa).status_code == 200

Loading…
Cancel
Save