mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
13 KiB
384 lines
13 KiB
import os
|
|
import re
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.contrib.auth.models import Permission
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.cache import cache
|
|
from django.http import Http404
|
|
from django.utils import timezone
|
|
from model_bakery import baker
|
|
import pytest
|
|
|
|
from sapl.base.media import MODELOS_DE_SESSAO, RESOLVEDORES, media
|
|
from sapl.base.models import AppConfig, Autor
|
|
from sapl.materia.models import Proposicao
|
|
from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo,
|
|
DocumentoAdministrativo)
|
|
from sapl.utils import EXTENSOES_ATIVAS
|
|
|
|
DOC_PATH = 'sapl/private/documentoadministrativo/2026/1/texto.pdf'
|
|
ACESSORIO_PATH = 'sapl/public/documentoacessorioadministrativo/2026/1/a.pdf'
|
|
PROPOSICAO_PATH = 'sapl/private/proposicao/2026/1/proposicao.pdf'
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def limpa_cache_appconfig():
|
|
# o cache em arquivo sobrevive ao rollback do teste
|
|
cache.clear()
|
|
yield
|
|
cache.clear()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def sem_debug(settings):
|
|
settings.DEBUG = False
|
|
|
|
|
|
def _config(comportamento):
|
|
cache.clear()
|
|
AppConfig.objects.all().delete()
|
|
baker.make(AppConfig, documentos_administrativos=comportamento)
|
|
|
|
|
|
def _login(client, username, *perms):
|
|
user = baker.make(get_user_model(), username=username)
|
|
for perm in perms:
|
|
app_label, codename = perm.split('.')
|
|
perm_obj = Permission.objects.filter(
|
|
content_type__app_label=app_label, codename=codename).first()
|
|
if not perm_obj:
|
|
perm_obj = Permission.objects.create(
|
|
codename=codename, name=codename,
|
|
content_type=ContentType.objects.get_for_model(
|
|
DocumentoAdministrativo))
|
|
user.user_permissions.add(perm_obj)
|
|
client.force_login(user)
|
|
return user
|
|
|
|
|
|
def _get(client, path):
|
|
return client.get('/media/' + path)
|
|
|
|
|
|
def _assert_servido(response, path):
|
|
assert response.status_code == 200
|
|
assert response['X-Accel-Redirect'] == '/media-interna/' + path
|
|
assert response['X-Content-Type-Options'] == 'nosniff'
|
|
assert response['Cache-Control'] == 'private'
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('comportamento,restrito,perfil,pode_ver', [
|
|
('O', False, None, True),
|
|
('O', True, None, False),
|
|
('O', True, 'logado', True),
|
|
('R', False, None, False),
|
|
('R', False, 'logado', False),
|
|
('R', True, None, False),
|
|
('R', True, 'logado', False),
|
|
('R', True, 'com_permissao', True),
|
|
])
|
|
def test_documento_administrativo(client, comportamento, restrito, perfil,
|
|
pode_ver):
|
|
_config(comportamento)
|
|
baker.make(DocumentoAdministrativo, restrito=restrito,
|
|
texto_integral=DOC_PATH)
|
|
if perfil == 'logado':
|
|
_login(client, 'logado')
|
|
elif perfil == 'com_permissao':
|
|
_login(client, 'perm',
|
|
'protocoloadm.detail_documentoadministrativo')
|
|
|
|
response = _get(client, DOC_PATH)
|
|
|
|
if pode_ver:
|
|
_assert_servido(response, DOC_PATH)
|
|
assert response['Content-Type'] == 'application/pdf'
|
|
else:
|
|
assert response.status_code == 404
|
|
assert not response.has_header('X-Accel-Redirect')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('restrito,pode_ver', [(False, True), (True, False)])
|
|
def test_documento_acessorio_segue_documento(client, restrito, pode_ver):
|
|
_config('O')
|
|
documento = baker.make(DocumentoAdministrativo, restrito=restrito)
|
|
baker.make(DocumentoAcessorioAdministrativo, documento=documento,
|
|
arquivo=ACESSORIO_PATH)
|
|
|
|
response = _get(client, ACESSORIO_PATH)
|
|
|
|
assert response.status_code == (200 if pode_ver else 404)
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_caminho_legado_e_prefixo_ponto_barra(client):
|
|
_config('O')
|
|
legado = 'sapl/public/documentoadministrativo/2010/7/antigo.pdf'
|
|
baker.make(DocumentoAdministrativo, texto_integral=legado)
|
|
baker.make(DocumentoAdministrativo, texto_integral='./' + DOC_PATH)
|
|
|
|
_assert_servido(_get(client, legado), legado)
|
|
_assert_servido(_get(client, DOC_PATH), DOC_PATH)
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('path', [
|
|
'sapl/private/documentoadministrativo/2026/1/inexistente.pdf',
|
|
'sapl/private/outromodelo/2026/1/texto.pdf',
|
|
'sapl/private/documentoadministrativo/../../public/x.pdf',
|
|
'sapl/private/documentoadministrativo/%2e%2e/x.pdf',
|
|
])
|
|
def test_caminho_invalido(client, path):
|
|
_config('O')
|
|
baker.make(DocumentoAdministrativo, texto_integral=DOC_PATH)
|
|
|
|
assert _get(client, path).status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_extensao_ativa_e_baixada(client):
|
|
_config('O')
|
|
path = 'sapl/private/documentoadministrativo/2026/1/x.html'
|
|
baker.make(DocumentoAdministrativo, texto_integral=path)
|
|
|
|
response = _get(client, path)
|
|
|
|
_assert_servido(response, path)
|
|
assert response['Content-Type'] == 'application/octet-stream'
|
|
assert response['Content-Disposition'].startswith('attachment;')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_nome_nao_ascii_codificado(client):
|
|
_config('O')
|
|
path = 'sapl/private/documentoadministrativo/2026/1/ofício_nº1.pdf'
|
|
baker.make(DocumentoAdministrativo, texto_integral=path)
|
|
|
|
response = _get(client, path)
|
|
|
|
assert response.status_code == 200
|
|
assert response['X-Accel-Redirect'] == (
|
|
'/media-interna/sapl/private/documentoadministrativo/2026/1/'
|
|
'of%C3%ADcio_n%C2%BA1.pdf')
|
|
|
|
|
|
ESTADOS_PROPOSICAO = ('rascunho', 'enviada', 'devolvida', 'incorporada')
|
|
PERMS_DOCADM = ['protocoloadm.add_documentoadministrativo',
|
|
'protocoloadm.change_documentoadministrativo',
|
|
'protocoloadm.detail_documentoadministrativo']
|
|
|
|
|
|
def _proposicao(estado):
|
|
autor = baker.make(Autor)
|
|
operador = baker.make(get_user_model(), username='operador')
|
|
autor.operadores.add(operador)
|
|
agora = timezone.now()
|
|
# mesmos campos gravados pelo envio, devolução e incorporação
|
|
baker.make(Proposicao, autor=autor, texto_original=PROPOSICAO_PATH,
|
|
data_envio=agora if estado in ('enviada', 'incorporada')
|
|
else None,
|
|
data_recebimento=agora if estado == 'incorporada' else None,
|
|
data_devolucao=agora if estado == 'devolvida' else None)
|
|
return operador
|
|
|
|
|
|
def _login_proposicao(client, perfil, operador):
|
|
if perfil == 'operador':
|
|
client.force_login(operador)
|
|
elif perfil == 'docadm':
|
|
user = _login(client, 'docadm', *PERMS_DOCADM)
|
|
assert user.has_module_perms('protocoloadm')
|
|
elif perfil == 'outro':
|
|
_login(client, 'outro')
|
|
elif perfil:
|
|
_login(client, perfil, 'materia.detail_proposicao_' + perfil)
|
|
|
|
|
|
def _pode_ver_proposicao(estado, perfil):
|
|
return perfil == 'operador' or perfil == estado != 'rascunho'
|
|
|
|
|
|
GRADE_PROPOSICAO = [
|
|
(estado, perfil, _pode_ver_proposicao(estado, perfil))
|
|
for estado in ESTADOS_PROPOSICAO
|
|
for perfil in (None, 'operador', 'outro', 'docadm', 'enviada',
|
|
'devolvida', 'incorporada')]
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('estado,perfil,pode_ver', GRADE_PROPOSICAO)
|
|
def test_proposicao(client, estado, perfil, pode_ver):
|
|
_login_proposicao(client, perfil, _proposicao(estado))
|
|
|
|
response = _get(client, PROPOSICAO_PATH)
|
|
|
|
if pode_ver:
|
|
_assert_servido(response, PROPOSICAO_PATH)
|
|
else:
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_arquivo_publico_passa_por_x_accel(client):
|
|
path = 'sapl/public/materialegislativa/2026/1/materia.pdf'
|
|
|
|
response = _get(client, path)
|
|
|
|
assert response.status_code == 200
|
|
assert response['X-Accel-Redirect'] == '/media-interna/' + path
|
|
assert response['Content-Type'] == 'application/pdf'
|
|
assert response['X-Content-Type-Options'] == 'nosniff'
|
|
assert not response.has_header('Cache-Control')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('nome', ['x.svg', 'x.html', 'x.XML', 'x.js',
|
|
'x.xht', 'x.rdf', 'x.xslt', 'x.kml'])
|
|
def test_arquivo_publico_ativo_e_baixado(client, nome):
|
|
response = _get(client, 'sapl/public/normajuridica/2026/1/' + nome)
|
|
|
|
assert response['Content-Type'] == 'application/octet-stream'
|
|
assert response['Content-Disposition'].startswith('attachment;')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('path', [
|
|
'sapl/public/../private/documentoadministrativo/2026/1/texto.pdf',
|
|
'sapl//private/documentoadministrativo/2026/1/texto.pdf',
|
|
'sapl/./private/documentoadministrativo/2026/1/texto.pdf',
|
|
'sapl/public/normajuridica/',
|
|
])
|
|
def test_caminho_nao_normalizado(client, path):
|
|
assert _get(client, path).status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_debug_serve_arquivo(rf, settings, tmp_path):
|
|
settings.DEBUG = True
|
|
settings.MEDIA_ROOT = str(tmp_path)
|
|
_config('R')
|
|
publico = tmp_path / 'sapl/public/normajuridica/1/n.pdf'
|
|
publico.parent.mkdir(parents=True)
|
|
publico.write_bytes(b'%PDF-1.4 norma')
|
|
baker.make(DocumentoAdministrativo, texto_integral=DOC_PATH)
|
|
|
|
def chama(path):
|
|
request = rf.get('/media/' + path)
|
|
request.user = AnonymousUser()
|
|
return media(request, path)
|
|
|
|
response = chama('sapl/public/normajuridica/1/n.pdf')
|
|
assert b''.join(response.streaming_content) == b'%PDF-1.4 norma'
|
|
assert not response.has_header('X-Accel-Redirect')
|
|
|
|
for path in (DOC_PATH, 'sapl/public/normajuridica/1/nao.pdf'):
|
|
with pytest.raises(Http404):
|
|
chama(path)
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('debug', [False, True])
|
|
def test_link_simbolico_para_fora_de_media(rf, settings, tmp_path, debug):
|
|
settings.DEBUG = debug
|
|
media_root = tmp_path / 'media'
|
|
fora = tmp_path / 'fora'
|
|
(media_root / 'sapl/public').mkdir(parents=True)
|
|
fora.mkdir()
|
|
(fora / 'segredo.txt').write_bytes(b'segredo')
|
|
(media_root / 'sapl/public/link').symlink_to(fora)
|
|
settings.MEDIA_ROOT = str(media_root)
|
|
request = rf.get('/media/sapl/public/link/segredo.txt')
|
|
request.user = AnonymousUser()
|
|
|
|
with pytest.raises(Http404):
|
|
media(request, 'sapl/public/link/segredo.txt')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('path', ['/etc/passwd', 'sapl/public//etc/passwd'])
|
|
def test_caminho_absoluto(rf, path):
|
|
request = rf.get('/media/' + path)
|
|
request.user = AnonymousUser()
|
|
|
|
with pytest.raises(Http404):
|
|
media(request, path)
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_modelo_protegido_fora_de_private_e_public(client):
|
|
_config('O')
|
|
path = 'sapl/outra/documentoadministrativo/2026/1/texto.pdf'
|
|
baker.make(DocumentoAdministrativo, restrito=True, texto_integral=path)
|
|
|
|
assert _get(client, path).status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_senha_fraca_nao_redireciona_media(client):
|
|
_login(client, 'fraca')
|
|
session = client.session
|
|
session['weak_password'] = True
|
|
session.save()
|
|
|
|
response = _get(client, 'sapl/public/casalegislativa/logotipo.png')
|
|
|
|
assert response.status_code == 200
|
|
assert response.has_header('X-Accel-Redirect')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
@pytest.mark.parametrize('estado,perfil,pode_ver', GRADE_PROPOSICAO)
|
|
def test_proposicao_texto(client, settings, tmp_path, estado, perfil,
|
|
pode_ver):
|
|
settings.MEDIA_ROOT = str(tmp_path)
|
|
arquivo = tmp_path / PROPOSICAO_PATH
|
|
arquivo.parent.mkdir(parents=True)
|
|
arquivo.write_bytes(b'%PDF-1.4 proposicao')
|
|
_login_proposicao(client, perfil, _proposicao(estado))
|
|
pk = Proposicao.objects.get(texto_original=PROPOSICAO_PATH).pk
|
|
|
|
response = client.get('/proposicao/texto/%s' % pk)
|
|
|
|
if pode_ver:
|
|
assert response.status_code == 200
|
|
assert response.content == b'%PDF-1.4 proposicao'
|
|
else:
|
|
assert response.status_code == 302
|
|
assert response['Location'] == '/proposicao/%s' % pk
|
|
|
|
|
|
def test_nginx_envia_ao_django_todos_os_caminhos_protegidos():
|
|
caminho = os.path.join(settings.PROJECT_DIR, 'docker', 'config',
|
|
'nginx', 'sapl.conf')
|
|
with open(caminho) as f:
|
|
conf = f.read()
|
|
regex = re.search(r'location ~\*? \^/media/sapl/\(private/\|'
|
|
r'\[\^/\]\+/\(([a-z|]+)\)/\)', conf)
|
|
|
|
assert regex, 'location de arquivos protegidos não encontrada'
|
|
no_nginx = set(regex.group(1).split('|'))
|
|
assert set(RESOLVEDORES) | set(MODELOS_DE_SESSAO) <= no_nginx
|
|
assert conf.index(regex.group(0)) < conf.index('location /media/ {')
|
|
|
|
|
|
def test_nginx_forca_download_das_extensoes_ativas():
|
|
caminho = os.path.join(settings.PROJECT_DIR, 'docker', 'config',
|
|
'nginx', 'sapl.conf')
|
|
with open(caminho) as f:
|
|
conf = f.read()
|
|
regex = re.search(r'location ~\*? \^/media/\.\+\\\.\(([a-z?|]+)\)\$',
|
|
conf)
|
|
|
|
assert regex, 'location de extensões ativas não encontrada'
|
|
no_nginx = set()
|
|
for ext in regex.group(1).split('|'):
|
|
if ext.endswith('?'):
|
|
no_nginx |= {ext[:-2], ext[:-1]}
|
|
else:
|
|
no_nginx.add(ext)
|
|
assert {'.' + e for e in no_nginx} == EXTENSOES_ATIVAS
|
|
|