feat: protect production Snipe-IT operations

This commit is contained in:
2026-08-05 12:37:12 +01:00
parent 26330b75f7
commit 8c69331118
13 changed files with 289 additions and 26 deletions

View File

@@ -77,11 +77,15 @@ Os relatórios usam consultas de leitura com período máximo de 366 dias. A exp
3. `AssetSyncService` faz upsert por `snipe_asset_id`, calcula `content_hash` e marca cada ativo com `last_seen_sync_run_id`.
4. Apenas após todas as páginas serem recebidas com sucesso, ativos cujo marcador não corresponde à execução atual ficam `active = false`; não são apagados para preservar histórico.
5. Antes de aprovação ou entrega, o ativo é validado novamente.
6. Checkout/check-in usam um token operacional separado e guardam apenas a resposta necessária para auditoria.
6. Imediatamente antes de cada checkout, o portal faz um `GET /hardware/{id}` com o token de leitura e bloqueia a operação se existir qualquer atribuição remota.
7. Checkout/check-in usam um token operacional separado e guardam apenas a resposta necessária para auditoria.
8. Antes de um check-in, o portal exige um checkout confirmado em `handover_records` para o mesmo requisitante e confirma por `GET` que o ativo continua atribuído a esse utilizador Snipe-IT.
`HandoverService` mantém a ordem explícita “Snipe-IT primeiro, confirmação local depois” para cada equipamento. Uma requisição com vários itens não é apresentada como concluída se apenas parte dos movimentos for aceite: os itens confirmados preservam o seu `HandoverRecord`, os restantes mantêm o estado anterior e a requisição passa a `incident`. As escritas externas não têm retry automático porque um timeout pode ocorrer depois de o Snipe-IT executar a operação; repetir sem reconciliação poderia criar um resultado enganador.
O checkout associa o ativo a um utilizador Snipe-IT. O operador confirma o ID remoto na primeira entrega e o portal guarda-o em `users.snipe_user_id` para utilizações futuras. O check-in repõe o estado e a localização habitual sincronizados do ativo. As respostas externas ficam em `handover_records.snipe_response`; tokens nunca são persistidos.
O checkout associa o ativo a um utilizador Snipe-IT. O operador confirma o ID remoto na primeira entrega e o portal guarda-o em `users.snipe_user_id` para utilizações futuras. Um ativo já atribuído nunca recebe um checkout do portal. O check-in repõe o estado e a localização habitual sincronizados do ativo, mas apenas quando o movimento de saída foi confirmado pelo portal e a atribuição remota ainda corresponde ao mesmo destinatário. As respostas externas ficam em `handover_records.snipe_response`; tokens nunca são persistidos.
A instância Snipe-IT ligada ao projeto é de produção e é tratada como inventário imutável. O portal não cria, edita nem elimina ativos, modelos, produtos ou outra informação mestre. Os únicos pedidos de escrita autorizados no cliente são os endpoints estritos de checkout e check-in, invocados por uma ação operacional legítima. Diagnósticos, sincronizações e validações técnicas usam exclusivamente `GET`.
Uma falha intermédia pode deixar upserts válidos das páginas anteriores, mas nunca executa a desativação global. A sincronização seguinte converge o cache. Um lock distribuído e um job único impedem duas sincronizações simultâneas.

View File

@@ -110,3 +110,12 @@ Por defeito, uma identidade Microsoft só pode ser associada a partir de uma con
Separar liveness, que prova apenas resposta do processo, de readiness, que confirma PostgreSQL e cache. As respostas públicas contêm apenas o estado agregado. O Nginx usa readiness no Docker Compose.
Disponibilizar scripts independentes para `pg_dump` custom e restauro numa base temporária validada. Os scripts usam nomes resolvidos do contentor, restringem os identificadores aceites, geram checksum e nunca substituem a base principal. Encriptação, armazenamento externo, retenção e alertas permanecem responsabilidades explícitas da plataforma de produção.
## ADR-016 — Snipe-IT de produção imutável salvo movimentos controlados
**Data:** 2026-08-05
**Estado:** aceite
A instância Snipe-IT configurada é de produção. Ficam proibidas, tanto na aplicação como em diagnóstico, operações de criação, edição ou eliminação de ativos, modelos, produtos e restantes dados mestres. Os únicos pedidos remotos de escrita admitidos são checkout e check-in resultantes de uma ação operacional legítima no portal; testes de conectividade e sincronizações são exclusivamente de leitura.
Cada checkout faz primeiro uma leitura atual de `hardware/{id}` e é recusado sem qualquer escrita se o ativo estiver atribuído. Cada check-in exige um `HandoverRecord` de checkout criado pelo portal para o mesmo requisitante e uma leitura atual que confirme a atribuição ao respetivo `snipe_user_id`. Uma divergência é tratada como incidente para reconciliação manual. Mantém-se a decisão de não repetir escritas automaticamente e de nunca expor tokens, URL privada ou respostas com dados de inventário em documentação, logs de diagnóstico ou mensagens de agentes.

View File

@@ -21,7 +21,10 @@ Referências oficiais: [Authorization Code Flow com PKCE](https://learn.microsof
- [ ] Criar conta/token de leitura com menor privilégio.
- [ ] Criar conta/token operacional separado, apenas com checkout/check-in necessários.
- [ ] Executar sincronização integral e comparar amostra de etiquetas, categorias, estados e localizações.
- [ ] Exercitar checkout/check-in com equipamentos de teste e reconciliar `handover_records`.
- [ ] Confirmar por inspeção e testes simulados que o portal não contém endpoints de criação, edição ou eliminação de inventário.
- [ ] Validar tecnicamente a produção apenas com pedidos `GET`; nunca usar um movimento artificial como teste de conectividade.
- [ ] No primeiro movimento institucional legítimo, usar um ativo existente e não atribuído, verificar o preflight e reconciliar `handover_records`.
- [ ] Confirmar que um ativo já atribuído é bloqueado sem qualquer pedido de escrita.
- [ ] Confirmar resposta a indisponibilidade, timeout e falha parcial sem retries de escrita.
## 3. Email, domínio e TLS
@@ -72,7 +75,7 @@ Referências oficiais: [Authorization Code Flow com PKCE](https://learn.microsof
| Área | Responsável | Data | Resultado | Evidência externa |
|---|---|---|---|---|
| Entra ID | Por atribuir | — | Pendente | — |
| Snipe-IT | Por atribuir | — | Pendente | — |
| Snipe-IT | Codex (validação técnica) | 2026-08-05 | Parcial: dois tokens distintos responderam a `GET`; sincronização de 2367 ativos; zero escritas | Registo interno em [PROJECT_STATUS.md](PROJECT_STATUS.md); validação institucional pendente |
| SMTP/TLS | Por atribuir | — | Pendente | — |
| Monitorização | Por atribuir | — | Pendente | — |
| Restauro | Por atribuir | — | Pendente | — |

View File

@@ -1,7 +1,7 @@
# Estado do projeto e handoff
**Atualizado em:** 4 de agosto de 2026
**Fase:** Fases 1 a 6 concluídas; Fase 7 em curso (base técnica de identidade, saúde e recuperação concluída)
**Atualizado em:** 5 de agosto de 2026
**Fase:** Fases 1 a 6 concluídas; Fase 7 em curso (identidade, saúde, recuperação e ligação de leitura ao Snipe-IT de produção concluídas)
**Último agente:** Codex
## Resultado disponível
@@ -35,6 +35,9 @@
- Substituição de equipamento apenas por ativo livre da mesma categoria, mantendo a pré-reserva e todo o histórico.
- Fila de aprovação com filtros por referência, requisitante, categoria e período, incluindo histórico de ações.
- Cliente Snipe-IT operacional com token separado, checkout para utilizador, check-in para a localização habitual e validação de erros transportados em HTTP 200.
- Preflight remoto obrigatório antes de checkout; ativos já atribuídos são recusados sem qualquer pedido de escrita.
- Check-in condicionado a checkout confirmado pelo portal para o mesmo requisitante e a atribuição remota atual ao respetivo utilizador Snipe-IT.
- Snipe-IT de produção formalmente limitado a leitura, checkout e check-in: criar, editar ou eliminar inventário é proibido no código, diagnóstico e procedimentos de agentes.
- Área diária do operador protegida pelo perfil `inventory_operator`/`administrator`, com leitura por scanner-teclado ou pesquisa de referência, requisitante, etiqueta e ID Snipe-IT.
- Preparação, entrega, devolução e conclusão com máquina de estados, condição física, observações, operador, destinatário e resposta externa em `handover_records`.
- Associação persistente entre o utilizador do portal e o respetivo ID no Snipe-IT, confirmada durante uma entrega bem-sucedida.
@@ -59,7 +62,7 @@
## Limites conhecidos
- A integração está implementada e testada contra respostas simuladas; falta configurar e validar URL, versão e token da instância institucional. Até lá, o catálogo utiliza dados fictícios do seeder.
- A URL e os dois tokens distintos estão configurados localmente e responderam com sucesso a pedidos `GET` em 5 de agosto de 2026. A sincronização integral de leitura importou 2367 ativos para o cache local, dos quais 716 estavam atribuídos e 144 marcados como requisitáveis. Não foi executada qualquer escrita real. Falta confirmar institucionalmente a versão, o menor privilégio efetivo dos tokens e as categorias/estados autorizados.
- Microsoft Entra ID está implementado e testado com tokens assinados simulados; falta registar/configurar a aplicação no tenant institucional e validar Conditional Access/MFA real.
- As notificações estão implementadas com Mailpit em desenvolvimento; falta validar SMTP institucional antes do piloto.
- O calendário atual é a vista mensal pessoal do MVP; vistas diária, semanal, global e por equipamento/categoria/localização ficam para as áreas operacional e administrativa.
@@ -72,7 +75,7 @@
Implementar a **Fase 7 — hardening e entrada em produção**:
1. configurar e validar Microsoft Entra no tenant institucional, incluindo Conditional Access/MFA;
2. validar a integração contra o Snipe-IT institucional e configurar tokens reais de menor privilégio;
2. confirmar com o responsável Snipe-IT a versão, o âmbito de menor privilégio dos tokens e as categorias/estados requisitáveis; validar checkout/check-in apenas no primeiro movimento institucional legítimo;
3. configurar SMTP institucional, domínio, HTTPS e gestão externa de segredos;
4. ligar os health checks a monitorização/alertas e agendar backups cifrados externos;
5. executar testes de segurança, concorrência, desempenho e end-to-end com equipamentos reais;
@@ -81,12 +84,12 @@ Implementar a **Fase 7 — hardening e entrada em produção**:
## Testes da última sessão
Validação concluída em 4 de agosto de 2026:
Validação concluída em 5 de agosto de 2026:
- `docker compose exec -T app composer test`: passou;
- Laravel Pint: 141 ficheiros sem desvios;
- PHPStan/Larastan nível 7: 107 ficheiros, zero erros;
- Pest: 103 testes passaram, 585 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada;
- Pest: 108 testes passaram, 609 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada;
- TypeScript (`tsc --noEmit`): passou;
- ESLint: passou;
- Prettier: passou;
@@ -109,6 +112,12 @@ Validação concluída em 4 de agosto de 2026:
- smoke tests via Nginx: `/health/live` devolveu `ok` e `/health/ready` devolveu `ready`.
- diagnóstico de login local: a base de desenvolvimento estava vazia apesar de migrada; `db:seed --force` repôs 2 utilizadores ativos, 5 perfis e 18 ativos fictícios, sem substituir dados existentes;
- login real no browser validado com `demo@escola.pt`: redirecionou para `/dashboard` e não produziu erros de consola.
- testes focados das salvaguardas Snipe-IT: 17 passaram, com 106 asserções, incluindo prova de ausência de `POST` em ativos atribuídos e check-in sem proveniência ou destinatário válidos;
- tokens Snipe-IT confirmados como configurados e distintos; ambos responderam a uma consulta `GET` com sucesso, sem revelar credenciais ou URL;
- sincronização real exclusivamente de leitura concluída: 2367 ativos processados e criados no cache local, sem qualquer pedido de escrita no Snipe-IT;
- cache local após sincronização: 2367 ativos ativos, 716 atribuídos e 144 marcados como requisitáveis;
- frontend: TypeScript, ESLint, Prettier e build Vite passaram novamente; Composer Audit sem avisos conhecidos;
- smoke tests após a sincronização: liveness `ok`, readiness `ready` e `/login` HTTP 200.
## Decisões pendentes do contexto institucional
@@ -124,4 +133,4 @@ Estas decisões não bloqueiam o desenvolvimento com defaults seguros, mas devem
## Próximo ficheiro a abrir
Começar em `docs/PRODUCTION_CHECKLIST.md`. A próxima alteração de código pode preparar testes de concorrência/desempenho e observabilidade local; as validações Entra, Snipe-IT e SMTP só ficam concluídas quando existirem serviços institucionais concretos, sem guardar os respetivos segredos no repositório.
Começar em `docs/PRODUCTION_CHECKLIST.md`. Para Snipe-IT, confirmar versão, categorias/estados e permissões efetivas com o responsável institucional; não executar testes de escrita. A próxima alteração de código pode preparar testes de concorrência/desempenho e observabilidade local. Entra, SMTP e o primeiro movimento Snipe-IT só ficam concluídos com validação institucional, sem guardar segredos no repositório.

View File

@@ -74,6 +74,8 @@ docker compose restart queue scheduler
## Integração Snipe-IT
> **Instância de produção:** é proibido criar, editar ou eliminar produtos, ativos ou quaisquer dados mestres. Diagnósticos e testes técnicos fazem apenas pedidos `GET`. Checkout e check-in só podem ocorrer como movimentos operacionais legítimos iniciados por um operador no portal. Um ativo que já esteja atribuído não pode ser usado para testar ou repetir um checkout.
Definir no ambiente, sem colocar tokens no repositório:
```dotenv
@@ -88,6 +90,8 @@ SNIPE_IT_RETRY_DELAY_MS=250
O token de leitura deve pertencer a uma conta técnica com acesso apenas de leitura aos ativos. O token operacional deve ser separado e ter apenas as permissões necessárias para checkout/check-in. A aplicação acrescenta `/api/v1` automaticamente quando a URL não o contém.
Nunca validar o token operacional através de um checkout/check-in artificial. A validade de leitura pode ser confirmada pelo teste de ligação administrativo ou por uma sincronização. A permissão operacional é confirmada apenas no primeiro movimento institucional legítimo, sobre um ativo existente, atualmente não atribuído e previamente reservado no portal.
Operações:
```bash
@@ -103,6 +107,8 @@ docker compose exec app php artisan schedule:list
Administradores podem testar a ligação, iniciar uma execução e consultar as últimas dez sincronizações em `http://localhost:8000/administracao/snipe-it`. Uma execução falhada mantém o último catálogo válido e não desativa registos ausentes. Consultar `sync_runs`, os logs do worker e o painel antes de repetir.
O teste de ligação e `snipe:sync-assets` consultam a API e alteram apenas o cache local do portal. Não executar comandos improvisados com `POST`, `PUT`, `PATCH` ou `DELETE` contra a URL configurada. Nunca copiar para terminais partilhados, tickets ou documentação os tokens, a URL privada ou corpos integrais devolvidos pela API.
## Microsoft Entra ID
Registar uma aplicação **single tenant** do tipo Web. O redirect URI tem de corresponder exatamente ao domínio do portal e ao caminho `/auth/microsoft/callback`. Em produção é obrigatório HTTPS.
@@ -139,6 +145,8 @@ Procedimento de entrega:
4. registar a entrega e verificar a mensagem de sucesso;
5. se existir erro, não voltar a submeter antes de consultar o estado do ativo no Snipe-IT.
Antes do `POST` de checkout, o portal consulta o estado remoto atual. Se o ativo já estiver atribuído — ao destinatário esperado ou a qualquer outro — a entrega é bloqueada sem escrita no Snipe-IT. Não contornar este bloqueio nem alterar o ativo diretamente para prosseguir.
Procedimento de devolução:
1. localizar a requisição ou ler a etiqueta;
@@ -147,6 +155,8 @@ Procedimento de devolução:
4. criar um incidente e bloqueio temporal quando o ativo não deva voltar ao catálogo;
5. concluir o processo apenas depois de todos os itens estarem devolvidos.
O check-in só é enviado se existir um checkout confirmado pelo portal para esse item e requisitante, e se a atribuição atual no Snipe-IT corresponder ao mesmo utilizador. Se alguma destas provas falhar, não efetuar o check-in manualmente a partir do portal; reconciliar o histórico com o responsável do inventário.
Uma operação parcial fica em `incident`. Consultar `handover_records` para movimentos confirmados e os eventos `reservation.delivery_failed`/`reservation.return_failed` em `audit_logs`. Comparar cada item com o estado atual no Snipe-IT; corrigir apenas os itens ainda `ready` ou `delivered`. Não alterar estados diretamente na base de dados nem repetir em bloco, porque as escritas externas são deliberadamente não idempotentes e não têm retry automático.
## Ciclo de vida das pré-reservas