diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 97aef7461..c7ec4d640 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -13,8 +13,8 @@ from django.db import models from django.http.response import Http404 from django.utils.decorators import classonlymethod 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 ugettext_lazy as _ from django.views.generic import (CreateView, DeleteView, DetailView, ListView, UpdateView) 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.utils import normalize + logger = logging.getLogger(__name__) ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ @@ -163,7 +164,7 @@ class PermissionRequiredForAppCrudMixin(PermissionRequiredMixin): apps = self.app_label if isinstance(apps, str): apps = apps, - # papp_label vazio dará acesso geral + # app_label vazio dará acesso geral for app in apps: if not self.request.user.has_module_perms(app): return False @@ -870,13 +871,36 @@ class 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 só 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): - permission_required = ('base.view_tabelas_auxiliares',) 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): 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 return context @@ -886,8 +910,8 @@ class CrudAux(Crud): ModelCrud = Crud.build( _model, _help_path, _model_set, list_field_names) - class ModelCrudAux(ModelCrud): - BaseMixin = CrudAux.BaseMixin + class ModelCrudAux(CrudAux, ModelCrud): + pass return ModelCrudAux diff --git a/sapl/test_urls.py b/sapl/test_urls.py index 55802b002..bdb45125b 100644 --- a/sapl/test_urls.py +++ b/sapl/test_urls.py @@ -1,11 +1,11 @@ -import pytest from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.management import _get_all_permissions from django.contrib.auth.models import Permission 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 ugettext_lazy as _ +import pytest from sapl.crud.base import PermissionRequiredForAppCrudMixin 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 + pytestmark = pytest.mark.django_db 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) 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 - # com /sistema/ + # Verifica a url é de um CrudAux e, se for, testa se está + # na página Tabelas Auxiliares key, url, var, app_name = url_item 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 """ % (url, app_name) - -@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}) - app_name = app_name[5:] - - apps_url_patterns_prefixs = { - 'base': [ +apps_url_patterns_prefixs_and_users = { + 'base': { + 'users': {'operador_geral': ['/sistema']}, + 'prefixs': [ '/sistema', '/login', '/logout' - ], - 'comissoes': [ + ]}, + 'comissoes': { + 'users': {'operador_geral': ['/sistema', '/comissao'], + 'operador_comissoes': ['/comissao']}, + 'prefixs': [ '/comissao', '/sistema' - ], - 'compilacao': [ + ]}, + 'compilacao': { + 'prefixs': [ '/ta', - ], - 'lexml': [ + ]}, + 'lexml': { + 'prefixs': [ '/lexml', '/sistema' - ], - 'materia': [ + ]}, + 'materia': { + 'users': {'operador_geral': ['/sistema', '/materia'], + 'operador_autor': ['/proposicao'], + 'operador_materia': ['/materia']}, + 'prefixs': [ '/materia', '/proposicao', '/sistema' - ], - 'norma': [ + ]}, + 'norma': { + 'users': {'operador_geral': ['/sistema', '/norma'], + 'operador_norma': ['/norma']}, + 'prefixs': [ '/norma', '/sistema' - ], - 'painel': [ + ]}, + 'painel': { + 'users': {'operador_geral': ['/sistema', '/painel'], + 'operador_painel': ['/painel']}, + 'prefixs': [ '/painel', '/sistema' - ], - 'parlamentares': [ + ]}, + 'parlamentares': { + 'users': {'operador_geral': ['/sistema', + '/mesa-diretora', + '/parlamentar']}, + 'prefixs': [ '/parlamentar', '/mesa-diretora', '/sistema' - ], - 'protocoloadm': [ + ]}, + 'protocoloadm': { + 'users': {'operador_geral': ['/sistema', + '/protocoloadm/docadm', + '/protocoloadm'], + 'operador_administrativo': ['/protocoloadm/docadm'], + 'operador_protocoloadm': ['/protocoloadm']}, + 'prefixs': [ '/protocoloadm', '/sistema' - ], - 'relatorios': [ + ]}, + 'relatorios': { + 'prefixs': [ '/relatorios', - ], - 'sessao': [ + ]}, + 'sessao': { + 'users': {'operador_geral': ['/sistema', 'sessao'], + 'operador_sessao': ['/sessao']}, + 'prefixs': [ '/sessao', '/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 """ % (app_name, url) - if app_name in apps_url_patterns_prefixs: - prefixs = apps_url_patterns_prefixs[app_name] + if app_name in apps_url_patterns_prefixs_and_users: + prefixs = apps_url_patterns_prefixs_and_users[app_name]['prefixs'] isvalid = False for prefix in prefixs: @@ -218,9 +256,23 @@ def test_urlpatterns(url_item, admin_client): %s """ % (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(): for app in sapl_appconfs: # readequa permissões dos models adicionando @@ -230,106 +282,140 @@ def em_construcao_crud_permissions_urls(urls_app, client): cria_grupos_permissoes() 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 - url = '/' + (url % {v: 1 for v in var}) + view = None + 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 só 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 hasattr(key, 'view_class'): - view_class = key.view_class + if isinstance(app_labels, str): + app_labels = 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 app in app_labels: - for username in users: - print(username, user_for_url_atual_app, url) + assert app in apps_url_patterns_prefixs_and_users, """ + 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 - try: - rg = client.get(url, {}, follow=True) - except: - pass + users_for_url_atual_app = apps_url_patterns_prefixs_and_users[ + app]['users'] - rp = None - try: - rp = client.post(url, {}, follow=True) - except: - pass + for username in users: + print(username, users_for_url_atual_app, url) - """ - devido às urls serem incompletas ou com pks e outras valores - inexistentes na base, iniciar a execução da view, seja por get, - post ou qualquer outro método pode causar o erro... - por isso o "try ... except" acima. - No entanto, o objetivo do teste é validar o acesso de toda url. - Independente do erro que vá acontecer, esse erro não ocorrerá - 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 - REDIRECIONAR PARA - LOGIN ANTES DE SUA EXECUÇÃO", desta forma nunca gerando erro - interno dada qualquer incoerência de parâmetros nas urls - """ - if rg: - """ - Se o usuário a ser testado é o usuário da app da url de get - espera-se que não tenha recebido uma tela de login - """ - if username == user_for_url_atual_app and\ - not url.startswith('/sistema/'): - assert btn_login not in str(rg.content) - elif username != 'operador_geral' and\ - url.startswith('/sistema/'): - assert btn_login in str(rg.content) - elif username == 'operador_geral' and\ - url.startswith('/sistema/'): - assert btn_login not in str(rg.content) - - if rp: - """ - Se o usuário a ser testado é o usuário da app da url de - post espera-se que não tenha recebido uma tela de login - """ - if username == user_for_url_atual_app and\ - not url.startswith('/sistema/'): - assert btn_login not in str(rp.content) - elif username != 'operador_geral' and\ - url.startswith('/sistema/'): - assert btn_login in str(rp.content) - elif username == 'operador_geral' and\ - url.startswith('/sistema/'): - assert btn_login not in str(rp.content) - - client.get('/logout/', follow=True) + client.login(username=username, password='interlegis') + + rg = None + try: + rg = client.get(url, {}, follow=True) + except: + pass + + rp = None + try: + rp = client.post(url, {}, follow=True) + except: + pass + + """ + devido às urls serem incompletas ou com pks e outras valores + inexistentes na base, iniciar a execução da view, seja por get, + post ou qualquer outro método pode causar o erro... + por isso o "try ... except" acima. + No entanto, o objetivo do teste é validar o acesso de toda url. + Independente do erro que vá acontecer, esse erro não ocorrerá + 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 + REDIRECIONAR PARA + LOGIN ANTES DE SUA EXECUÇÃO", desta forma nunca gerando erro + interno dada qualquer incoerência de parâmetros nas urls + """ + + for _type, content in ( + ('get', str(rg.content if rg else '')), + ('post', str(rp.content if rp else ''))): + + if not content: + continue + + def _assert_login(_in): + if _in: + assert btn_login in content, """ + No teste de requisição "%s" a url (%s). + App (%s) + O usuário (%s) deveria ser redirecionado + para tela de login. + """ % (_type, url, app, username) + 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) diff --git a/scripts/lista_urls.py b/scripts/lista_urls.py index 4c940f0c2..769268e1b 100644 --- a/scripts/lista_urls.py +++ b/scripts/lista_urls.py @@ -25,7 +25,10 @@ class ListaUrls(): if value: url = value[0][0][0] var = value[0][0][1] + # if url.endswith('anexada/create'): + # if url.startswith('materia/confirmar/'): urls.append((key, url, var, item.app_name)) + urls.sort(key=lambda x: x[1]) return urls