Browse Source

Add teste de perm as urls e fixa bug em CrudAux

O teste adicionado faz login com todos os perfis criados para testar os
grupos triviais. Segundo o mapa de usuários e prefixos de urls
permitidas, espera-se que não haja, ou haja, redirecionamento para login
mediante as configurações do mapa de usuários.

O mapa que serve tanto para o novo teste de conexão, quanto para o de
validação de prefixos de urls, é uma mapa base e não é absoluto.
Se algo novo for criado ou exitir uma alteração que não se encaixe no
mapa, o teste quebrará, portanto, caberá ao autor da inovação decidir
se, para passar pelo teste o mapa deve ser mudado, ou se algo deve ser
feito em sua implementação para se adequar ao mapa.
pull/726/head
LeandroRoberto 8 years ago
parent
commit
48fb3962a7
  1. 34
      sapl/crud/base.py
  2. 356
      sapl/test_urls.py
  3. 3
      scripts/lista_urls.py

34
sapl/crud/base.py

@ -13,8 +13,8 @@ from django.db import models
from django.http.response import Http404 from django.http.response import Http404
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DeleteView, DetailView, ListView, from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
UpdateView) UpdateView)
from django.views.generic.base import ContextMixin from django.views.generic.base import ContextMixin
@ -23,6 +23,7 @@ from django.views.generic.list import MultipleObjectMixin
from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display
from sapl.utils import normalize from sapl.utils import normalize
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \
@ -163,7 +164,7 @@ class PermissionRequiredForAppCrudMixin(PermissionRequiredMixin):
apps = self.app_label apps = self.app_label
if isinstance(apps, str): if isinstance(apps, str):
apps = apps, apps = apps,
# papp_label vazio dará acesso geral # app_label vazio dará acesso geral
for app in apps: for app in apps:
if not self.request.user.has_module_perms(app): if not self.request.user.has_module_perms(app):
return False return False
@ -870,13 +871,36 @@ class Crud:
class CrudAux(Crud): class CrudAux(Crud):
"""
Checa permissão para ver qualquer dado de tabela auxiliar
a permissão base.view_tabelas_auxiliares está definada class Meta
do model sapl.base.models.AppConfig que, naturalmente é um arquivo
de configuração geral e pode ser acessado através das Tabelas
Auxiliares... Com isso o script de geração de perfis acaba que por
criar essa permissão apenas para o perfil Operador Geral.
"""
permission_required = ('base.view_tabelas_auxiliares',)
class BaseMixin(Crud.BaseMixin): class BaseMixin(Crud.BaseMixin):
permission_required = ('base.view_tabelas_auxiliares',)
subnav_template_name = None subnav_template_name = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
"""
Mantem as permissões individuais geradas pelo Crud através do
Modelo e adiciona a obrigatoriedade de permissão para view
tabelas auxiliares.
"""
self.permission_required = self.permission_required + \
self.crud.permission_required
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
"""Força o template filter subnav em base/templatetags/menus.py
a abrir um yaml diferente do padrão.
Se o valor de subnav_template_name é nulo faz o filter subnav
não abrir o padrão e nem um outro arquivo.
"""
context['subnav_template_name'] = self.subnav_template_name context['subnav_template_name'] = self.subnav_template_name
return context return context
@ -886,8 +910,8 @@ class CrudAux(Crud):
ModelCrud = Crud.build( ModelCrud = Crud.build(
_model, _help_path, _model_set, list_field_names) _model, _help_path, _model_set, list_field_names)
class ModelCrudAux(ModelCrud): class ModelCrudAux(CrudAux, ModelCrud):
BaseMixin = CrudAux.BaseMixin pass
return ModelCrudAux return ModelCrudAux

356
sapl/test_urls.py

@ -1,11 +1,11 @@
import pytest
from django.apps import apps from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.management import _get_all_permissions from django.contrib.auth.management import _get_all_permissions
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
import pytest
from sapl.crud.base import PermissionRequiredForAppCrudMixin from sapl.crud.base import PermissionRequiredForAppCrudMixin
from scripts.inicializa_grupos_autorizacoes import cria_grupos_permissoes from scripts.inicializa_grupos_autorizacoes import cria_grupos_permissoes
@ -13,6 +13,7 @@ from scripts.lista_urls import lista_urls
from .settings import SAPL_APPS from .settings import SAPL_APPS
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
sapl_appconfs = [apps.get_app_config(n[5:]) for n in SAPL_APPS] sapl_appconfs = [apps.get_app_config(n[5:]) for n in SAPL_APPS]
@ -111,8 +112,8 @@ def test_crudaux_formato_inicio_urls_associadas(url_item):
@pytest.mark.parametrize('url_item', _lista_urls) @pytest.mark.parametrize('url_item', _lista_urls)
def test_crudaux_list_do_crud_esta_na_pagina_sistema(url_item, admin_client): def test_crudaux_list_do_crud_esta_na_pagina_sistema(url_item, admin_client):
# Verifica se um crud é do tipo CrudAux, se sim, sua url deve começar # Verifica a url é de um CrudAux e, se for, testa se está
# com /sistema/ # na página Tabelas Auxiliares
key, url, var, app_name = url_item key, url, var, app_name = url_item
url = '/' + (url % {v: 1 for v in var}) url = '/' + (url % {v: 1 for v in var})
@ -144,67 +145,104 @@ def test_crudaux_list_do_crud_esta_na_pagina_sistema(url_item, admin_client):
Se encontra em %s.urls Se encontra em %s.urls
""" % (url, app_name) """ % (url, app_name)
apps_url_patterns_prefixs_and_users = {
@pytest.mark.parametrize('url_item', _lista_urls) 'base': {
def test_urlpatterns(url_item, admin_client): 'users': {'operador_geral': ['/sistema']},
'prefixs': [
key, url, var, app_name = url_item
url = '/' + (url % {v: 1 for v in var})
app_name = app_name[5:]
apps_url_patterns_prefixs = {
'base': [
'/sistema', '/sistema',
'/login', '/login',
'/logout' '/logout'
], ]},
'comissoes': [ 'comissoes': {
'users': {'operador_geral': ['/sistema', '/comissao'],
'operador_comissoes': ['/comissao']},
'prefixs': [
'/comissao', '/comissao',
'/sistema' '/sistema'
], ]},
'compilacao': [ 'compilacao': {
'prefixs': [
'/ta', '/ta',
], ]},
'lexml': [ 'lexml': {
'prefixs': [
'/lexml', '/lexml',
'/sistema' '/sistema'
], ]},
'materia': [ 'materia': {
'users': {'operador_geral': ['/sistema', '/materia'],
'operador_autor': ['/proposicao'],
'operador_materia': ['/materia']},
'prefixs': [
'/materia', '/materia',
'/proposicao', '/proposicao',
'/sistema' '/sistema'
], ]},
'norma': [ 'norma': {
'users': {'operador_geral': ['/sistema', '/norma'],
'operador_norma': ['/norma']},
'prefixs': [
'/norma', '/norma',
'/sistema' '/sistema'
], ]},
'painel': [ 'painel': {
'users': {'operador_geral': ['/sistema', '/painel'],
'operador_painel': ['/painel']},
'prefixs': [
'/painel', '/painel',
'/sistema' '/sistema'
], ]},
'parlamentares': [ 'parlamentares': {
'users': {'operador_geral': ['/sistema',
'/mesa-diretora',
'/parlamentar']},
'prefixs': [
'/parlamentar', '/parlamentar',
'/mesa-diretora', '/mesa-diretora',
'/sistema' '/sistema'
], ]},
'protocoloadm': [ 'protocoloadm': {
'users': {'operador_geral': ['/sistema',
'/protocoloadm/docadm',
'/protocoloadm'],
'operador_administrativo': ['/protocoloadm/docadm'],
'operador_protocoloadm': ['/protocoloadm']},
'prefixs': [
'/protocoloadm', '/protocoloadm',
'/sistema' '/sistema'
], ]},
'relatorios': [ 'relatorios': {
'prefixs': [
'/relatorios', '/relatorios',
], ]},
'sessao': [ 'sessao': {
'users': {'operador_geral': ['/sistema', 'sessao'],
'operador_sessao': ['/sessao']},
'prefixs': [
'/sessao', '/sessao',
'/sistema', '/sistema',
], ]},
} }
assert app_name in apps_url_patterns_prefixs, """
@pytest.mark.parametrize('url_item', _lista_urls)
def test_urlpatterns(url_item, admin_client):
key, url, var, app_name = url_item
url = '/' + (url % {v: 1 for v in var})
assert '\n' not in url, """
A url (%s) da app (%s) está mal formada.
""" % (app_name, url)
app_name = app_name[5:]
assert app_name in apps_url_patterns_prefixs_and_users, """
A app (%s) da url (%s) não consta na lista de prefixos do teste A app (%s) da url (%s) não consta na lista de prefixos do teste
""" % (app_name, url) """ % (app_name, url)
if app_name in apps_url_patterns_prefixs: if app_name in apps_url_patterns_prefixs_and_users:
prefixs = apps_url_patterns_prefixs[app_name] prefixs = apps_url_patterns_prefixs_and_users[app_name]['prefixs']
isvalid = False isvalid = False
for prefix in prefixs: for prefix in prefixs:
@ -218,9 +256,23 @@ def test_urlpatterns(url_item, admin_client):
%s %s
""" % (url, app_name, prefixs) """ % (url, app_name, prefixs)
urls_publicas_sem_permission_required = [
'/login',
'/logout',
'/comissao/1/materias-em-tramitacao',
'/materia/confirmar/1/1',
]
@pytest.mark.parametrize('url_item', _lista_urls)
def test_permissions_urls_for_users_by_apps(url_item, client):
key, url, var, app_name = url_item
url = '/' + (url % {v: 1 for v in var})
if url in urls_publicas_sem_permission_required:
return
@pytest.mark.parametrize('urls_app', _lista_urls)
def em_construcao_crud_permissions_urls(urls_app, client):
if not get_user_model().objects.exists(): if not get_user_model().objects.exists():
for app in sapl_appconfs: for app in sapl_appconfs:
# readequa permissões dos models adicionando # readequa permissões dos models adicionando
@ -230,106 +282,140 @@ def em_construcao_crud_permissions_urls(urls_app, client):
cria_grupos_permissoes() cria_grupos_permissoes()
users = get_user_model().objects.values_list('username', flat=True) users = get_user_model().objects.values_list('username', flat=True)
for url_item in _lista_urls[urls_app]: app_labels = app_name.split('.')[1]
key, url, var, app_name = url_item view = None
url = '/' + (url % {v: 1 for v in var}) if hasattr(key, 'view_class'):
view = key.view_class()
"""
A classe PermissionRequiredForAppCrudMixin pode ser usada em uma
app mas envolver permissoes para outras
como é o caso de PainelView que está na app 'sessao'
mas é um redirecionamento para 'painel'... aqui é feita
a troca da app a ser testada, por essas outras possíveis.
Este, até a ultima versão deste teste é o único tipo de view que
possui restrição restrição simples, por permissão, e não por
container, como é o caso de proposições que possui restrição
por usuário e não por, ou não tem, o campo permission_required
"""
if PermissionRequiredForAppCrudMixin in type.mro(key.view_class):
# essa classe deve informar app_label
assert hasattr(key.view_class, 'app_label')
# app_label deve ter conteudo
assert key.view_class.app_label
app_labels = key.view_class.app_label
else:
app_labels = app_name.split('.')[1] if hasattr(view, 'permission_required') and \
view.permission_required is not None and\
len(view.permission_required) == 0:
"""
condição do Crud, se tem permission_required e ele é igual [],
então é uma view pública, teste liberado.
"""
return
else:
"""
Views que não se encaixam nãs condições acima, podem possuir
ou não restrição de acesso. Se o código continuar,
será tratado como tentativa de validar pois é possível
ter restrição local, como uma anotação method_required.
Caberá ao desenvolvedor de uma nova view, se for pública e
sem necessidade de nenhum tratamento de permissão, para limpar
o teste to py.test adicionar sua url
representativa na variavel externa ao teste:
urls_publicas_sem_permission_required, logo acima do teste
"""
pass
view_class = None if isinstance(app_labels, str):
if hasattr(key, 'view_class'): app_labels = app_labels,
view_class = key.view_class
""" for app in app_labels:
A classe PermissionRequiredForAppCrudMixin pode ser usada em uma
app mas envolver permissoes para outras
como é o caso de PainelView que está na app 'sessao'
mas é um redirecionamento para 'painel'... aqui é feita
a troca a urls_app a ser testada, por essas outras possíveis
"""
if PermissionRequiredForAppCrudMixin in type.mro(view_class):
# essa classe deve informar app_label
assert hasattr(view_class, 'app_label')
# app_label deve ter conteudo
assert view_class.app_label
app_labels = view_class.app_label
if isinstance(app_labels, str):
app_labels = app_labels,
for app in app_labels:
# monta o username correspondente de a app da url a ser testada
user_for_url_atual_app = 'operador_%s'
if app in ['base', 'parlamentares']:
user_for_url_atual_app = user_for_url_atual_app % 'geral'
elif app in 'protocoloadm':
user_for_url_atual_app = (
user_for_url_atual_app % 'administrativo')
elif app in ['compilacao']:
return # TODO implementar teste para compilacao
else:
user_for_url_atual_app = user_for_url_atual_app % app
for username in users: assert app in apps_url_patterns_prefixs_and_users, """
print(username, user_for_url_atual_app, url) O app_label (%s) associado a url (%s) não está na base de testes.
%s
""" % (app_name, url)
client.login(username=username, password='interlegis') if 'users' not in apps_url_patterns_prefixs_and_users[app]:
continue
rg = None users_for_url_atual_app = apps_url_patterns_prefixs_and_users[
try: app]['users']
rg = client.get(url, {}, follow=True)
except:
pass
rp = None for username in users:
try: print(username, users_for_url_atual_app, url)
rp = client.post(url, {}, follow=True)
except:
pass
""" client.login(username=username, password='interlegis')
devido às urls serem incompletas ou com pks e outras valores
inexistentes na base, iniciar a execução da view, seja por get, rg = None
post ou qualquer outro método pode causar o erro... try:
por isso o "try ... except" acima. rg = client.get(url, {}, follow=True)
No entanto, o objetivo do teste é validar o acesso de toda url. except:
Independente do erro que acontecer, esse erro não ocorrerá pass
se o user não tiver permissão de acesso pelo fato de que "AS
VIEWS BEM FORMADAS PARA VALIDAÇÃO DE ACESSO DEVEM SEMPRE rp = None
REDIRECIONAR PARA try:
LOGIN ANTES DE SUA EXECUÇÃO", desta forma nunca gerando erro rp = client.post(url, {}, follow=True)
interno dada qualquer incoerência de parâmetros nas urls except:
""" pass
if rg:
""" """
Se o usuário a ser testado é o usuário da app da url de get devido às urls serem incompletas ou com pks e outras valores
espera-se que não tenha recebido uma tela de login inexistentes na base, iniciar a execução da view, seja por get,
""" post ou qualquer outro método pode causar o erro...
if username == user_for_url_atual_app and\ por isso o "try ... except" acima.
not url.startswith('/sistema/'): No entanto, o objetivo do teste é validar o acesso de toda url.
assert btn_login not in str(rg.content) Independente do erro que acontecer, esse erro não ocorrerá
elif username != 'operador_geral' and\ se o user não tiver permissão de acesso pelo fato de que "AS
url.startswith('/sistema/'): VIEWS BEM FORMADAS PARA VALIDAÇÃO DE ACESSO DEVEM SEMPRE
assert btn_login in str(rg.content) REDIRECIONAR PARA
elif username == 'operador_geral' and\ LOGIN ANTES DE SUA EXECUÇÃO", desta forma nunca gerando erro
url.startswith('/sistema/'): interno dada qualquer incoerência de parâmetros nas urls
assert btn_login not in str(rg.content) """
if rp: for _type, content in (
""" ('get', str(rg.content if rg else '')),
Se o usuário a ser testado é o usuário da app da url de ('post', str(rp.content if rp else ''))):
post espera-se que não tenha recebido uma tela de login
""" if not content:
if username == user_for_url_atual_app and\ continue
not url.startswith('/sistema/'):
assert btn_login not in str(rp.content) def _assert_login(_in):
elif username != 'operador_geral' and\ if _in:
url.startswith('/sistema/'): assert btn_login in content, """
assert btn_login in str(rp.content) No teste de requisição "%s" a url (%s).
elif username == 'operador_geral' and\ App (%s)
url.startswith('/sistema/'): O usuário (%s) deveria ser redirecionado
assert btn_login not in str(rp.content) para tela de login.
""" % (_type, url, app, username)
client.get('/logout/', follow=True) else:
assert btn_login not in content, """
No teste de requisição "%s" a url (%s).
App (%s)
O usuário (%s) não deveria ser redirecionado
para tela de login. Se essa é uma url
invariavelmente pública, a adicione na variavel
abaixo localizada no arquivo que se encontra este
teste:
urls_publicas_sem_permission_required
""" % (_type, url, app, username)
if username not in users_for_url_atual_app:
# se não é usuário da app deve ser redirecionado para login
_assert_login(True)
else:
prefixs = users_for_url_atual_app[username]
for pr in prefixs:
if url.startswith(pr):
_assert_login(False)
break
client.get('/logout/', follow=True)

3
scripts/lista_urls.py

@ -25,7 +25,10 @@ class ListaUrls():
if value: if value:
url = value[0][0][0] url = value[0][0][0]
var = value[0][0][1] var = value[0][0][1]
# if url.endswith('anexada/create'):
# if url.startswith('materia/confirmar/'):
urls.append((key, url, var, item.app_name)) urls.append((key, url, var, item.app_name))
urls.sort(key=lambda x: x[1]) urls.sort(key=lambda x: x[1])
return urls return urls

Loading…
Cancel
Save