Skip to content

Centraliza a autorização por equipes e atualiza a administração para o Wagtail 7.1 - #1064

Merged
pitangainnovare merged 73 commits into
scieloorg:rc-team-authorizationfrom
pitangainnovare:feature/team-authorization-wagtail-7
Aug 27, 2026
Merged

Centraliza a autorização por equipes e atualiza a administração para o Wagtail 7.1#1064
pitangainnovare merged 73 commits into
scieloorg:rc-team-authorizationfrom
pitangainnovare:feature/team-authorization-wagtail-7

Conversation

@pitangainnovare

@pitangainnovare pitangainnovare commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

O que esse PR faz?

Centraliza as regras de autorização administrativa com base nas equipes de coleção, periódico e empresa, aplicando o princípio de menor privilégio às telas e operações do Wagtail. Além da validação manual indicada em uma das seções seguintes, pode ser interessante testar manualmente a partir do roteiro disponibilizado neste link.

Este PR:

  • atualiza a administração para o Wagtail 7.1;
  • institui grupos canônicos para os perfis operacionais;
  • centraliza a matriz e a resolução de autorização;
  • sincroniza automaticamente usuários, membresias e grupos;
  • protege os grupos controlados pelo sistema contra alterações manuais;
  • restringe menus, listagens, formulários, relacionamentos e ações administrativas ao escopo editorial do usuário;
  • permite ao administrador e ao superusuário acessar objetos independentemente do responsável designado;
  • corrige a delegação da análise de qualidade, preservando o analista selecionado;
  • impede o recebimento de pacotes para periódicos não autorizados;
  • interrompe o processamento antes de consultar ou revelar dados editoriais de outro periódico;
  • mantém o pacote rejeitado visível somente ao remetente para consulta do erro;
  • substitui helpers administrativos legados pelas políticas nativas de permissão do Wagtail;
  • adiciona um comando de reconciliação para corrigir grupos, permissões e vínculos existentes;
  • remove código, wrappers, imports e templates administrativos que deixaram de ter consumidores.

Onde a revisão poderia começar?

A revisão pode começar pelos seguintes módulos:

  1. team/authorization_matrix.py

    • Define os perfis, recursos e operações autorizadas para equipes de coleção, periódico e empresa.
  2. team/authorization.py

    • Resolve a autorização efetiva do usuário a partir da matriz e das membresias ativas.
  3. team/policies.py

    • Integra as regras de equipe às políticas de permissão utilizadas pela administração.
  4. core/users/permission_policies.py

    • Implementa as políticas reutilizáveis para querysets e objetos sujeitos a escopo.
  5. upload/querysets.py e upload/permission_policies.py

    • Aplicam o isolamento dos pacotes e as permissões administrativas do upload.
  6. upload/controller.py

    • Bloqueia, antes das demais consultas e validações, uploads destinados a periódicos não autorizados.
  7. team/signals.py e team/management/commands/create_user_groups.py

    • Sincronizam e reconciliam os grupos canônicos e seus usuários.

As migrações relacionadas estão em:

  • team/migrations/0005_create_team_groups.py;
  • team/migrations/0006_normalize_active_members.py;
  • team/migrations/0007_validate_contract_date_range.py;
  • upload/migrations/0011_alter_package_options.py.

Como este poderia ser testado manualmente?

Preparação

  1. Executar as migrações.
  2. Executar o comando de reconciliação dos grupos.
  3. Confirmar que os grupos canônicos foram criados com as permissões esperadas.

Administração de grupos e equipes

  1. Acessar o Wagtail como superusuário.
  2. Confirmar que os grupos canônicos não podem ser renomeados ou excluídos manualmente.
  3. Criar ou atualizar membresias de coleção, periódico e empresa.
  4. Confirmar que os usuários são incluídos nos grupos correspondentes.
  5. Desativar ou expirar uma membresia e confirmar que o vínculo de grupo é removido na reconciliação.

Escopo administrativo

  1. Entrar como integrante de uma equipe de periódico.
  2. Confirmar que artigos, fascículos, pacotes e opções de formulário ficam limitados aos periódicos autorizados.
  3. Confirmar que objetos de outros periódicos não aparecem nas listagens, buscas, menus ou seletores.
  4. Repetir a validação com integrantes de equipes de coleção e empresa.
  5. Entrar como administrador ou superusuário e confirmar que todos os objetos autorizados ficam visíveis, inclusive os atribuídos a outros analistas.

Delegação da análise de qualidade

  1. Entrar como usuário autorizado a distribuir a análise.
  2. Abrir um pacote disponível para controle de qualidade.
  3. Selecionar somente outro analista, sem preencher a decisão de publicação.
  4. Salvar o formulário.
  5. Confirmar que o analista selecionado foi preservado e que o usuário atual não foi atribuído indevidamente.
  6. Confirmar que a publicação em QA continua automática, conforme a regra de negócio.
  7. Confirmar que a publicação no ambiente oficial continua dependendo da aprovação específica.

Autorização de upload

  1. Entrar como usuário vinculado somente ao periódico bn.
  2. Enviar um pacote pertencente ao periódico mr.
  3. Confirmar que o pacote recebe o estado unexpected.
  4. Confirmar que nenhuma validação, resolução de fascículo, publicação em QA ou consulta de pacote anterior é iniciada.
  5. Confirmar que o remetente consegue consultar o próprio pacote rejeitado e sua mensagem de erro.
  6. Confirmar que o pacote não aparece na administração do periódico mr.
  7. Enviar um pacote do periódico bn e confirmar que o processamento segue normalmente.
  8. Repetir o teste com autorização proveniente de equipe de coleção e de contrato ativo de empresa.

Validação automatizada realizada

Foram executados em Docker:

pytest -q team/tests upload/tests issue/tests collection/tests.py
97 passed

Também foram executados:

python manage.py check
python manage.py makemigrations --check --dry-run

O Django não identificou novas migrações pendentes. O manage.py check apresentou apenas avisos já conhecidos do projeto, sem erros bloqueantes.

Algum cenário de contexto que queira dar?

Antes desta alteração, parte da autorização era baseada em helpers específicos de cada aplicação, filtros de interface e verificações de grupo espalhadas pelo código.

Esse modelo permitia divergências entre:

  • a visibilidade dos objetos;
  • as ações apresentadas pelos botões;
  • o acesso direto às views;
  • as opções disponíveis nos formulários;
  • a autorização aplicada durante o processamento em segundo plano.

Um caso identificado permitia que um usuário enviasse um pacote pertencente a outro periódico. Embora o pacote não aparecesse normalmente na listagem administrativa, o processamento aceitava o arquivo e podia publicá-lo em QA.

A nova estrutura centraliza a decisão de autorização, aplica o mesmo escopo às diferentes camadas e adota negação por padrão quando não existe usuário ou membresia válida.

Este PR tem como base a branch rc-team-authorization, criada a partir de rc. Depois da homologação, a integração com rc deverá ocorrer em um PR separado.

Screenshots

N/A

Quais são os tickets relevantes?

#393 #851 #863

Referências

  • Documentação oficial do Wagtail 7.1.
  • Políticas nativas de permissão e ViewSets do Wagtail.
  • Princípio de menor privilégio.
  • Negação de acesso por padrão (fail-closed).
  • NSI.04 — Norma de Desenvolvimento Seguro.

Segurança da informação (NSI.04)

Seção obrigatória. Marque as opções aplicáveis e justifique quando necessário. Referência: NSI.04 - Norma de Desenvolvimento Seguro.

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Sim — utiliza usuários e vínculos de equipe já existentes para determinar o escopo de acesso. Não introduz novas categorias de dados pessoais. A exposição é reduzida por meio de querysets restritos, autorização por objeto e aplicação do princípio de menor privilégio.
  • Não

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Sim — centraliza a autorização por equipes de coleção, periódico e empresa; aplica escopo às listagens, formulários e ações; protege grupos canônicos; bloqueia uploads para periódicos não autorizados; e preserva o acesso administrativo global previsto para administradores e superusuários.
  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Sim — atualiza wagtail de 6.4.2 para 7.1.1, wagtail-django-recaptcha de 2.1.1 para 2.2.0 e wagtail-modeladmin de 2.0.0 para 2.2.0.
    • Verificado e aprovado
    • Pendente / vulnerabilidade aceita com justificativa: aguardando a execução do SBOM/Trivy no pipeline do PR.
  • Não

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Sim — link do job: preencher após a execução do pipeline.
  • Não aplicável a este PR (justifique):

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Sim — confirme que há sanitização/parametrização (prepared statements, escaping, etc.):
  • Não

Este PR expõe novos endpoints, telas ou serviços?

  • Sim — reorganiza e disponibiliza telas administrativas de equipes e ações de pacotes no Wagtail. O acesso é protegido pelas políticas de autorização, pelo escopo por objeto e pelo princípio de menor privilégio. O HTTPS permanece sob responsabilidade da configuração de implantação já adotada pelo projeto.
  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado
  • Sim (bloquear merge e corrigir antes de prosseguir)

Implementação: atualiza somente o Wagtail e as extensões administrativas diretamente necessárias.
Implementação: exibe a contagem somente quando o contexto fornece os totais necessários.
Implementação: centraliza os nomes dos grupos controlados pelo sistema em uma definição única.
Implementação: adiciona migração que cria os grupos e consolida o grupo empresarial legado.
Implementação: corrige valores nulos existentes e torna explícito o estado padrão dos vínculos editoriais.
Implementação: adiciona restrição de banco que impede término contratual anterior ao início.
Implementação: filtra contratos pela situação e pelo intervalo de vigência na data consultada.
Implementação: combina coleções, periódicos e empresas ativos sem prioridade que descarte vínculos simultâneos.
Implementação: move os identificadores de permissão para um módulo próprio do domínio de artigos.
Implementação: declara as ações de pacote e adiciona a migração das permissões do modelo.
Implementação: declara aplicações, modelos e ações permitidos aos administradores e membros de coleção.
Implementação: declara aplicações, modelos e ações permitidos aos administradores e membros de periódico.
Implementação: declara aplicações, modelos e ações permitidos aos administradores e membros de empresa.
Implementação: calcula níveis de acesso, ações de modelo e permissões Django a partir dos grupos ativos.
Implementação: converte o módulo de testes em pacote e preserva integralmente os casos existentes.
Implementação: cobre a estrutura completa e a precedência do maior acesso entre grupos simultâneos.
Implementação: registra sinais idempotentes que refletem papéis ativos nos grupos canônicos do usuário.
Implementação: reconcilia usuários substituídos e membros afetados pela exclusão de equipes ou coleções.
Implementação: bloqueia alterações manuais de nomes, vínculos, permissões e exclusão dos grupos canônicos.
Implementação: cobre mudanças de papel, inativação, substituição, exclusão e mutações manuais proibidas.
Implementação: cria comando idempotente baseado na matriz, incorpora o grupo legado e sincroniza vínculos existentes.
Implementação: cobre a matriz exata de permissões, idempotência e remoção de grupos sem vínculo ativo.
Implementação: adiciona alvos para migrar a base e sincronizar grupos, permissões e membresias.
Implementação: substitui a configuração padrão do Wagtail por viewsets administrativos protegidos.
Implementação: oculta grupos reservados e bloqueia atribuição, edição, renomeação e exclusão manual.
Implementação: filtra querysets por coleção, periódico ou empresa conforme os vínculos ativos do usuário.
Implementação: integra a matriz ao Wagtail e aplica escopo de queryset e objeto aos viewsets protegidos.
Implementação: relaciona itens administrativos às aplicações acessíveis pelos grupos ativos do usuário.
Implementação: restringe membresias, empresas e contratos aos vínculos que cada perfil pode administrar.
Implementação: filtra coleções, periódicos, empresas e usuários conforme o escopo do responsável.
Comment thread team/policies.py
def scope_queryset(user, queryset):
if user and user.is_superuser:
return queryset
if not user or not user.is_authenticated:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare mudar a ordem de verificação

Comment thread team/policies.py
return queryset.none()

group_names = set(user.groups.values_list("name", flat=True))
if TeamGroups.COLLECTION_ADMIN in group_names and (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare COLLECTION_ADMIN?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sim, COLLECTION_ADMIN.

Essa é a constante interna de TeamGroups, cujo valor é "COLLECTION_TEAM_ADMIN".

Conforme definido no issue #863, esse grupo pode realizar CRUD de Company. Como Company é um cadastro global e não possui vínculo direto com coleção, o administrador de uma coleção com membership ativa acessa o cadastro de empresas; usuários de empresa continuam limitados à própria empresa.

Comment thread team/forms.py Outdated
)


class ManagedRelationAdminForm(CoreAdminModelForm):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare com o desuso de modeladmin ainda faz sentdio o uso de coreadminmodelform? Viu o core.views?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ajustado. Removi a dependência de CoreAdminModelForm desses componentes. Os formulários agora usam diretamente WagtailAdminModelForm, e os ViewSets de equipe reutilizam CommonControlFieldViewSet, seguindo o fluxo nativo de snippets.

Comment thread upload/controller.py
Comment on lines +292 to +303
if (
not user
or not getattr(user, "is_authenticated", False)
or (
not user.is_superuser
and (
not journal
or journal.id not in membership.get("journal_list_ids", [])
)
)
):
journal_title = journal.title if journal else _("Unknown")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare se este código for recorrente poderia ter um método em User para passar o journal e realizar as checagens

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verifiquei e, atualmente, a checagem de acesso a um journal específico ocorre somente neste fluxo. Os demais usos trabalham com escopo de querysets ou apenas verificam a existência de algum journal autorizado. Preferi não adicionar o método em User, mantendo o modelo de core desacoplado das regras de equipe e contratos. Se a checagem específica se repetir, podemos centralizá-la em team.authorization.

Comment thread upload/controller.py
response.update(
pp.is_registered_xml_with_pre(xml_with_pre, xml_with_pre.filename)
)
logging.info(f"is_registered_xml_with_pre: {response}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare pode eliminar estes vários logging info e idealmente convertê-lo para uma lista que adicionada a algum modelo para visualizar o passo-a-passo pela interface. Mas isso pode ser um novo issue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Vamos deixar isso para outro PR específico para essa parte de logs.

Comment thread upload/forms.py Outdated
)

if not self.for_user or not self.for_user.is_superuser:
if self.for_user:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare if not self.for_user a linha 77 nunca vai acontecer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concordo. Nas views administrativas do Wagtail, for_user é sempre preenchido com request.user, e não há instanciação desse formulário sem o usuário no projeto. Vou remover o ramo inalcançável e manter apenas a verificação de superusuário.

Comment thread core/users/scoped_queryset.py Outdated
Comment on lines +13 to +16
if user and user.is_superuser:
return queryset
if not user or not user.is_authenticated:
return queryset.none()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare inverter a verificação

Comment thread upload/views.py Outdated
if request.method != "POST":
return render(
request,
"modeladmin/upload/package/confirm_action.html",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare tem que trocar o modeladmin porque vai falhar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ajustado. Removi o namespace legado modeladmin e movi o template para upload/admin/package/confirm_action.html. A herança de wagtailadmin/base.html foi mantida, pois continua válida: wagtailadmin é o namespace do painel administrativo, enquanto o legado removido é o ModelAdmin.

Comment thread upload/views.py Outdated
if request.method != "POST":
return render(
request,
"modeladmin/upload/package/confirm_action.html",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare trocar modeladmin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ajustado também. Esta referência agora utiliza upload/admin/package/confirm_action.html

Comment thread upload/views.py Outdated

return render(
request,
"modeladmin/upload/package/republish_selected.html",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare modeladmin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ajustado. Removi o namespace legado modeladmin e movi o template para upload/admin/package/republish_selected.html

Comment thread upload/wagtail_hooks.py Outdated

params = {}
try:
params["pkg_zip__id"] = request.GET["pkg_zip_id"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare trocar params["pkg_zip__id"] = por params["pkg_zip_id"] =

Comment thread upload/wagtail_hooks.py Outdated
Comment on lines +509 to +510
qs = super().get_queryset(request)
return qs.filter(package__in=get_scoped_package_queryset(request.user))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare parece repetitivo, poderia ser feito no BaseUploadViewSet?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sim.

O filtro se repete nos ViewSets de relatórios e resultados.

Vou centralizá-lo no BaseUploadViewSet, deixando configurável o caminho até o pacote, pois alguns modelos usam package e outros report__package.

Comment thread upload/models.py
@staticmethod
def is_free(pid_v2):
logging.info(f"PidV2Generator.is_free?")
logging.info("PidV2Generator.is_free?")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare idealmente trocar logging.info por uma lista e guardar em algum modelo para consulta do passo-a-passo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Vamos deixar isso para outro PR específico para essa parte de logs.

@robertatakenaka robertatakenaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitangainnovare fazer correções e verificar comentários

@pitangainnovare
pitangainnovare merged commit a32c93c into scieloorg:rc-team-authorization Aug 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants