docs: documentação de API para desenvolvedores (GLPI -> Redmine)
Endpoints, payloads, cabeçalhos, mapeamento de campos, impersonation, idempotência e exemplos completos. Material de treino para a equipe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
92d1ac044a
commit
4a285f65ac
1 changed files with 256 additions and 0 deletions
256
docs/API.md
Normal file
256
docs/API.md
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
# Documentação de API — Plugin Redmine Integration
|
||||||
|
|
||||||
|
> Documento de desenvolvedor. Descreve **o que sai do GLPI** e **como entra no Redmine**:
|
||||||
|
> gatilhos, chamadas à API REST do Redmine, payloads, cabeçalhos e o mapeamento de campos.
|
||||||
|
> Base de código: `inc/redmineapi.class.php` (cliente HTTP) e `hook.php` (orquestração).
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
1. [Visão geral](#1-visão-geral)
|
||||||
|
2. [Autenticação e cabeçalhos](#2-autenticação-e-cabeçalhos)
|
||||||
|
3. [Gatilhos no GLPI](#3-gatilhos-no-glpi)
|
||||||
|
4. [Resolução do projeto Redmine](#4-resolução-do-projeto-redmine)
|
||||||
|
5. [Endpoints do Redmine usados](#5-endpoints-do-redmine-usados)
|
||||||
|
6. [Mapeamento de campos GLPI → Redmine](#6-mapeamento-de-campos-glpi--redmine)
|
||||||
|
7. [Impersonation (autoria)](#7-impersonation-autoria)
|
||||||
|
8. [Idempotência e tabelas de estado](#8-idempotência-e-tabelas-de-estado)
|
||||||
|
9. [Tratamento de erros](#9-tratamento-de-erros)
|
||||||
|
10. [Exemplos completos](#10-exemplos-completos)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visão geral
|
||||||
|
|
||||||
|
O plugin é um **cliente da API REST do Redmine**. Ele reage a eventos do GLPI
|
||||||
|
(hooks) e traduz esses eventos em chamadas HTTP ao Redmine.
|
||||||
|
|
||||||
|
```
|
||||||
|
hooks GLPI HTTP (REST/JSON)
|
||||||
|
GLPI (chamados) ───────────► Plugin (PHP/cURL) ──────────────────► Redmine
|
||||||
|
inc/redmineapi.class.php
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Sentido único** (GLPI → Redmine) no fluxo automático. O GLPI é a fonte; o
|
||||||
|
Redmine recebe issues e lançamentos de tempo.
|
||||||
|
- A única exceção de leitura é a **Sincronização de Metadados** (Redmine → cache local).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Autenticação e cabeçalhos
|
||||||
|
|
||||||
|
Todas as chamadas usam a **chave de API de um administrador** do Redmine
|
||||||
|
(armazenada cifrada com `GLPIKey` em `glpi_plugin_redmine_configs.api_key`).
|
||||||
|
|
||||||
|
Cabeçalhos padrão:
|
||||||
|
|
||||||
|
| Cabeçalho | Valor | Quando |
|
||||||
|
|---|---|---|
|
||||||
|
| `X-Redmine-API-Key` | `<api_key do admin>` | sempre |
|
||||||
|
| `Content-Type` | `application/json` | POST/PUT |
|
||||||
|
| `X-Redmine-Switch-User` | `<login do usuário Redmine>` | impersonation (ver §7) |
|
||||||
|
|
||||||
|
- Timeout: **5s** por requisição (evita 504).
|
||||||
|
- Sucesso = HTTP `2xx`. O corpo é JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Gatilhos no GLPI
|
||||||
|
|
||||||
|
| Evento GLPI (hook) | Função | Ação no Redmine |
|
||||||
|
|---|---|---|
|
||||||
|
| `item_add` / `item_update` em **TicketTask** | `plugin_redmine_tickettask_*` | cria a issue (1ª vez) + lança o tempo |
|
||||||
|
| `item_update` em **Ticket** | `plugin_redmine_ticket_update` | atualiza o **status** da issue vinculada |
|
||||||
|
| Aba **Redmine** em Projeto/Entidade (form) | `front/project.form.php`, `front/entity.form.php` | cria/vincula projeto, define categoria, sincroniza metadados |
|
||||||
|
|
||||||
|
**Gate do lançamento de tempo:** o processamento da TicketTask só ocorre quando
|
||||||
|
`task.state == Planning::DONE` (Feito) **e** `actiontime > 0`. Tarefas não
|
||||||
|
concluídas permanecem apenas no GLPI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Resolução do projeto Redmine
|
||||||
|
|
||||||
|
Antes de criar a issue, o plugin descobre **em qual projeto Redmine** ela entra,
|
||||||
|
a partir do chamado:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ticket
|
||||||
|
├─ vinculado a um Projeto GLPI? ──► glpi_plugin_redmine_projects ─► redmine_project_id
|
||||||
|
│ (projeto via glpi_projecttasks_tickets ou glpi_itils_projects)
|
||||||
|
└─ senão ──► entities_id do ticket ─► glpi_plugin_redmine_entities ─► redmine_project_id
|
||||||
|
(match EXATO da entidade, sem herança de árvore)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sem vínculo (nem projeto nem entidade) → **nada é enviado** (log e retorno).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Endpoints do Redmine usados
|
||||||
|
|
||||||
|
### Leitura (Sincronizar Metadados)
|
||||||
|
| Método | Endpoint | Uso |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/projects.json?limit=100` | lista de projetos (cache + dropdowns) |
|
||||||
|
| GET | `/trackers.json` | tipos de tarefa |
|
||||||
|
| GET | `/issue_statuses.json` | status (alvo do mapa de status) |
|
||||||
|
| GET | `/enumerations/issue_priorities.json` | prioridades |
|
||||||
|
| GET | `/enumerations/time_entry_activities.json` | atividades de tempo |
|
||||||
|
| GET | `/roles.json` | papéis (para auto-membership) |
|
||||||
|
| GET | `/users.json?limit=100&offset=N` | usuários (cache p/ mapeamento; paginado) |
|
||||||
|
| GET | `/projects/{id}/issue_categories.json` | categorias (por projeto) |
|
||||||
|
|
||||||
|
### Escrita
|
||||||
|
| Método | Endpoint | Uso |
|
||||||
|
|---|---|---|
|
||||||
|
| POST | `/projects.json` | criar projeto Redmine (aba Projeto/Entidade) |
|
||||||
|
| POST | `/issues.json` | **criar a issue** a partir do chamado |
|
||||||
|
| PUT | `/issues/{id}.json` | sincronizar **status** da issue |
|
||||||
|
| POST | `/time_entries.json` | **lançar tempo** da TicketTask |
|
||||||
|
| POST | `/projects/{id}/memberships.json` | adicionar técnico como membro (auto-membership) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Mapeamento de campos GLPI → Redmine
|
||||||
|
|
||||||
|
### Issue (`POST /issues.json`)
|
||||||
|
| Campo Redmine (`issue.*`) | Origem no GLPI |
|
||||||
|
|---|---|
|
||||||
|
| `project_id` | projeto Redmine resolvido (§4) |
|
||||||
|
| `subject` | `Ticket.name` (truncado a 255, HTML removido) |
|
||||||
|
| `description` | `Ticket.content` (HTML removido) |
|
||||||
|
| `tracker_id` | `config.default_tracker_id` |
|
||||||
|
| `priority_id` | `config.default_priority_id` |
|
||||||
|
| `status_id` | `config.status_map[Ticket.status]` |
|
||||||
|
| `category_id` | `default_category_id` do vínculo (projeto/entidade) |
|
||||||
|
| `watcher_user_ids` | `config.default_watcher_ids` (array) |
|
||||||
|
| **autor** | técnico **atribuído** ao chamado → `X-Redmine-Switch-User` (§7) |
|
||||||
|
|
||||||
|
### Lançamento de tempo (`POST /time_entries.json`)
|
||||||
|
| Campo Redmine (`time_entry.*`) | Origem no GLPI |
|
||||||
|
|---|---|
|
||||||
|
| `issue_id` | issue da issue do chamado |
|
||||||
|
| `hours` | `TicketTask.actiontime / 3600` (segundos → horas decimais) |
|
||||||
|
| `comments` | `TicketTask.content` (HTML removido) |
|
||||||
|
| `activity_id` | `config.default_activity_id` |
|
||||||
|
| **autor** | quem lançou a task (`TicketTask.users_id`) → `X-Redmine-Switch-User` |
|
||||||
|
|
||||||
|
### Status (`PUT /issues/{id}.json`)
|
||||||
|
| Campo | Origem |
|
||||||
|
|---|---|
|
||||||
|
| `status_id` | `config.status_map[Ticket.status]` (no `item_update` do Ticket) |
|
||||||
|
|
||||||
|
### Conversões importantes
|
||||||
|
- **Tempo:** GLPI grava `actiontime` em **segundos** (int); Redmine espera **horas decimais**. `5400 → 1.5`.
|
||||||
|
- **Subject:** limite de 255 chars no Redmine → truncado.
|
||||||
|
- **HTML:** descrições do GLPI (TinyMCE) passam por `RichText::getTextFromHtml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Impersonation (autoria)
|
||||||
|
|
||||||
|
Por padrão, tudo criado via API nasce como o **admin** (dono da chave). Para
|
||||||
|
atribuir o autor correto, o plugin usa o cabeçalho **`X-Redmine-Switch-User: <login>`**.
|
||||||
|
|
||||||
|
**Pré-requisito:** o usuário impersonado precisa ser **membro do projeto Redmine**
|
||||||
|
com um papel — senão o Redmine retorna **403**. Por isso, antes de impersonar, o
|
||||||
|
plugin chama `ensureProjectMembership()` (adiciona o técnico com `config.default_role_id`).
|
||||||
|
|
||||||
|
**Mapeamento GLPI → usuário Redmine** (`resolveRedmineUser`):
|
||||||
|
1. **Override manual** (`glpi_plugin_redmine_usermap`), se houver.
|
||||||
|
2. **Auto-match por e-mail** (e-mail do usuário GLPI = `mail` do usuário Redmine, do cache).
|
||||||
|
|
||||||
|
Sem correspondência → cai no admin (fallback, §9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Idempotência e tabelas de estado
|
||||||
|
|
||||||
|
| Tabela | Garante |
|
||||||
|
|---|---|
|
||||||
|
| `glpi_plugin_redmine_tickets` (`tickets_id` único) | **1 issue por chamado** (reuso) |
|
||||||
|
| `glpi_plugin_redmine_tasks` (`tickettasks_id` único) | tempo lançado **1× por task** (sem duplicar) |
|
||||||
|
| `glpi_plugin_redmine_projects` / `_entities` | vínculo GLPI ↔ projeto Redmine |
|
||||||
|
| `glpi_plugin_redmine_users` / `_usermap` | cache de usuários + overrides de mapeamento |
|
||||||
|
|
||||||
|
Editar horas de uma task já lançada **não** re-sincroniza. Reverter `Feito`→`A fazer`
|
||||||
|
**não** apaga o time entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Tratamento de erros
|
||||||
|
|
||||||
|
- **Impersonation falha** (403/412 — usuário sem par no Redmine ou não-membro):
|
||||||
|
o plugin **refaz a chamada como admin** (não perde a issue/tempo) e registra
|
||||||
|
aviso em `files/_log/redmine*`.
|
||||||
|
- **API inacessível / chave inválida:** `syncMetadata()` retorna `false`; criação
|
||||||
|
de issue/tempo é abortada com log.
|
||||||
|
- Todo erro HTTP `>= 400` é logado via `Toolbox::logError`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Exemplos completos
|
||||||
|
|
||||||
|
### Criar issue (com impersonation)
|
||||||
|
```http
|
||||||
|
POST /issues.json HTTP/1.1
|
||||||
|
X-Redmine-API-Key: <admin_key>
|
||||||
|
X-Redmine-Switch-User: joao.silva
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"issue": {
|
||||||
|
"project_id": 3,
|
||||||
|
"subject": "Erro no servidor de e-mail",
|
||||||
|
"description": "Cliente relata falha no envio.",
|
||||||
|
"tracker_id": 1,
|
||||||
|
"priority_id": 2,
|
||||||
|
"status_id": 1,
|
||||||
|
"category_id": 7,
|
||||||
|
"watcher_user_ids": [5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Resposta `201`: `{ "issue": { "id": 42, "author": { "id": 9, "name": "João Silva" }, ... } }`
|
||||||
|
|
||||||
|
### Lançar tempo (com impersonation)
|
||||||
|
```http
|
||||||
|
POST /time_entries.json HTTP/1.1
|
||||||
|
X-Redmine-API-Key: <admin_key>
|
||||||
|
X-Redmine-Switch-User: joao.silva
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"time_entry": {
|
||||||
|
"issue_id": 42,
|
||||||
|
"hours": 1.5,
|
||||||
|
"activity_id": 1,
|
||||||
|
"comments": "Reinício do serviço e validação."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sincronizar status da issue
|
||||||
|
```http
|
||||||
|
PUT /issues/42.json
|
||||||
|
X-Redmine-API-Key: <admin_key>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{ "issue": { "status_id": 5 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-membership (antes de impersonar)
|
||||||
|
```http
|
||||||
|
POST /projects/3/memberships.json
|
||||||
|
X-Redmine-API-Key: <admin_key>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{ "membership": { "user_id": 9, "role_ids": [3] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Resumo do fluxo end-to-end (suporte recorrente / entidade)
|
||||||
|
1. Técnico marca a TicketTask como **Feito** no GLPI.
|
||||||
|
2. Hook resolve o projeto Redmine pela **entidade** do chamado.
|
||||||
|
3. (1ª vez) `POST /issues.json` cria a issue — autor = técnico atribuído (impersonation + auto-membership).
|
||||||
|
4. `POST /time_entries.json` lança as horas — autor = quem lançou a task.
|
||||||
|
5. Ao mudar o status do chamado, `PUT /issues/{id}.json` reflete no Redmine.
|
||||||
Loading…
Reference in a new issue