283 lines
9.5 KiB
Markdown
283 lines
9.5 KiB
Markdown
# Implementation Plan: Desk Auth & RBAC (003)
|
|
|
|
**Branch**: `003-desk-auth-rbac` | **Date**: 2026-06-10 | **Spec**: [spec.md](./spec.md)
|
|
|
|
**Input**: Feature specification from `specs/003-desk-auth-rbac/spec.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Proteger a API e UI do Ligbox Ops Desk com autenticação JWT e RBAC em 4 perfis. Utilizadores seed: `root` (super_admin), `admin` (ops_lead), `mini` (technician), `noc` (noc). Webhooks VM112/Wazuh mantêm auth por secret. UI ganha ecrã de login e envia Bearer token.
|
|
|
|
**Abordagem**: módulo `app/auth.py` (hash, JWT, dependencies FastAPI), middleware/dependencies `require_auth` + `require_role`, refactor `main.py` para proteger rotas humanas, frontend `login.html` + token em `sessionStorage`, script `verify-auth.sh`.
|
|
|
|
---
|
|
|
|
## Technical Context
|
|
|
|
| Item | Valor |
|
|
|------|-------|
|
|
| **Language** | Python 3.11+ (API), Vanilla JS (frontend) |
|
|
| **Framework** | FastAPI, uvicorn |
|
|
| **Auth** | JWT HS256 (`python-jose[cryptography]` ou `PyJWT`), bcrypt (`passlib`) |
|
|
| **Storage** | SQLite — nova tabela `desk_users`; `tickets.assigned_to` |
|
|
| **Deploy** | VM122 `/opt/ligbox-ops-platform/`, docker-compose rebuild `api` + `frontend` |
|
|
| **URLs** | LAN `10.10.10.122:8080/8091`; público `desk.ligbox.com.br`, `api.ops.ligbox.com.br` |
|
|
| **Testing** | `scripts/verify-auth.sh` — matrix 401/403 por role |
|
|
|
|
**New env vars** (`.env`):
|
|
|
|
```env
|
|
JWT_SECRET=<openssl rand -hex 32>
|
|
JWT_EXPIRE_HOURS=8
|
|
DESK_AUTH_ENABLED=true
|
|
OPS_INTERNAL_TOKEN=<optional worker bypass>
|
|
AUTH_LOGIN_RATE_LIMIT=5
|
|
```
|
|
|
|
---
|
|
|
|
## Constitution Check
|
|
|
|
| Princípio | Status | Notas |
|
|
|-----------|--------|-------|
|
|
| I. vmbr1 / LAN | ✅ PASS | Sem alteração rede |
|
|
| II. Interfaces Proxmox | ✅ PASS | N/A |
|
|
| III. Anti-scan Hetzner | ✅ PASS | Sem novas regras |
|
|
| IV. Mail vs Ops separation | ✅ PASS | Auth só no Ops |
|
|
| V. fail2ban | ✅ PASS | Inalterado; rate limit login complementar |
|
|
| VI. pfSense API | N/A | |
|
|
| VII. Spec-Driven | ✅ PASS | spec → plan → tasks |
|
|
| VIII. Documentation | ✅ PASS | `specs/003-*` |
|
|
| IX. YAGNI | ✅ PASS | JWT simples; sem OAuth/MFA |
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
### Documentation
|
|
|
|
```text
|
|
specs/003-desk-auth-rbac/
|
|
├── spec.md
|
|
├── plan.md # este ficheiro
|
|
├── research.md
|
|
├── data-model.md
|
|
├── quickstart.md
|
|
├── contracts/
|
|
│ └── auth-api.md
|
|
├── checklists/
|
|
│ └── requirements.md
|
|
└── tasks.md
|
|
```
|
|
|
|
### Source Code (VM122)
|
|
|
|
```text
|
|
api/
|
|
├── requirements.txt # + python-jose, passlib[bcrypt]
|
|
└── app/
|
|
├── main.py # proteger rotas; import auth deps
|
|
├── auth.py # NOVO: users, JWT, RBAC, mask noc
|
|
├── auth_routes.py # NOVO: /api/v1/auth/*
|
|
└── permissions.py # NOVO: ROLE_MATRIX, require_role decorator
|
|
|
|
frontend/
|
|
├── index.html # shell pós-login
|
|
├── login.html # NOVO: formulário login
|
|
└── assets/
|
|
├── app.js # token, redirect, role UI gates
|
|
├── auth.js # NOVO: login/logout/session
|
|
└── styles.css # estilos login
|
|
|
|
scripts/
|
|
└── verify-auth.sh # NOVO: testes 401/403 matrix
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### Dois canais de autenticação
|
|
|
|
```text
|
|
┌─────────────────────────────────────┐
|
|
│ Ligbox Ops API │
|
|
├─────────────────────────────────────┤
|
|
Browser (humano) │ Authorization: Bearer <JWT> │
|
|
───────────────► │ + role check per endpoint │
|
|
├─────────────────────────────────────┤
|
|
VM112 / Wazuh │ X-Webhook-Secret: <secret> │
|
|
───────────────► │ (sem JWT; inalterado) │
|
|
├─────────────────────────────────────┤
|
|
Traefik/monitor │ GET /health — público │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### Fluxo login
|
|
|
|
```text
|
|
1. POST /api/v1/auth/login {username, password}
|
|
2. Verificar desk_users (active=1, bcrypt verify)
|
|
3. Emitir JWT {sub, role, exp}
|
|
4. Update last_login_at
|
|
5. Return {access_token, token_type, role, username, expires_in}
|
|
6. Frontend guarda em sessionStorage; redirect /
|
|
```
|
|
|
|
### FastAPI dependencies
|
|
|
|
```python
|
|
# Pseudocódigo
|
|
async def get_current_user(authorization: str = Header(None)) -> DeskUser:
|
|
# parse Bearer JWT → DeskUser
|
|
|
|
def require_roles(*roles: str):
|
|
def dep(user: DeskUser = Depends(get_current_user)):
|
|
if user.role not in roles: raise HTTPException(403)
|
|
return user
|
|
return dep
|
|
|
|
# Webhook routes: skip JWT if valid X-Webhook-Secret
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 0: Research Summary
|
|
|
|
Ver [research.md](./research.md):
|
|
|
|
1. API 100% aberta hoje — confirmado em `api.ops.ligbox.com.br`
|
|
2. JWT em header (não cookie) — mais simples com SPA estática nginx
|
|
3. Worker audit chama `POST /api/v1/audit/cycle` — usar `OPS_INTERNAL_TOKEN` header interno
|
|
4. Traefik pode proxy `/api` no mesmo host do desk — CORS simplificado
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation — Auth backend
|
|
|
|
**Goal**: Tabela users, login, JWT, dependencies.
|
|
|
|
| Task | Detalhe |
|
|
|------|---------|
|
|
| 1.1 | `auth.py`: `DeskUser` model, bcrypt hash/verify |
|
|
| 1.2 | `init_db()`: CREATE `desk_users`; seed 4 users se vazio |
|
|
| 1.3 | `auth_routes.py`: `POST /login`, `POST /logout` (noop client), `GET /me` |
|
|
| 1.4 | JWT create/verify com `JWT_SECRET` |
|
|
| 1.5 | Rate limit login: 5/min/IP (dict in-memory MVP ou Redis INCR) |
|
|
| 1.6 | `permissions.py`: `ROLE_PERMISSIONS` dict + helpers `can_patch_ticket(user, ticket)` |
|
|
|
|
**Seed passwords** (bootstrap):
|
|
|
|
```python
|
|
SEED_USERS = [
|
|
("root", "super_admin"),
|
|
("admin", "ops_lead"),
|
|
("mini", "technician"),
|
|
("noc", "noc"),
|
|
]
|
|
# password from DESK_BOOTSTRAP_PASSWORD env or default 805353 (log warning)
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Protect API routes
|
|
|
|
**Goal**: Todas as rotas humanas exigem JWT + role.
|
|
|
|
| Endpoint group | Roles permitidos |
|
|
|----------------|------------------|
|
|
| `/api/v1/desk/*` GET | all authenticated; noc → masked payload |
|
|
| `/api/v1/desk/tickets/{id}` PATCH | super_admin, ops_lead; technician se assigned |
|
|
| `/api/v1/onboard/*` GET | super_admin, ops_lead; technician funnel parcial; noc summary |
|
|
| `/api/v1/audit/*` GET | super_admin, ops_lead, noc (overview masked) |
|
|
| `/api/v1/audit/*` POST | super_admin, ops_lead |
|
|
| `/api/v1/tenants` GET | all authenticated |
|
|
| `/api/v1/webhooks/events` GET | super_admin, ops_lead, technician; noc wazuh filter |
|
|
| `/api/v1/infra/*` GET | all authenticated |
|
|
| `/api/v1/integrations` GET | all authenticated |
|
|
| `/api/v1/auth/users` | super_admin only |
|
|
|
|
**Público (sem JWT)**:
|
|
|
|
- `GET /health`, `GET /api/health`
|
|
- `POST /api/v1/auth/login`
|
|
- `POST /api/v1/webhooks/onboard`
|
|
- `POST /api/v1/webhooks/ingress/{integration}`
|
|
|
|
**Worker bypass**:
|
|
|
|
- `POST /api/v1/audit/cycle` aceita `X-Ops-Internal-Token` == `OPS_INTERNAL_TOKEN`
|
|
|
|
**Implementação**:
|
|
|
|
- Refactor: `APIRouter` com `dependencies=[Depends(get_current_user)]` por grupo
|
|
- `_enrich_ticket()`: chamar `_mask_for_role(ticket, user.role)` se noc
|
|
- Migration tickets: `ALTER TABLE` ou recreate column `assigned_to TEXT`
|
|
|
|
---
|
|
|
|
## Phase 3: Frontend login & session
|
|
|
|
**Goal**: UI não carrega sem login.
|
|
|
|
| Task | Detalhe |
|
|
|------|---------|
|
|
| 3.1 | `login.html` — form username/password, POST login, guardar token |
|
|
| 3.2 | `auth.js` — `getToken()`, `isLoggedIn()`, `logout()`, `authHeaders()` |
|
|
| 3.3 | `app.js` — no boot: se sem token → `location.href='/login.html'` |
|
|
| 3.4 | `api()` helper — inject `Authorization: Bearer` |
|
|
| 3.5 | Sidebar: mostrar `username (role)` + botão Sair |
|
|
| 3.6 | Role gates UI: esconder nav Infra/Tenants para noc se restrito; esconder PATCH buttons |
|
|
| 3.7 | nginx: `login.html` como entry; `index.html` requer JS auth check |
|
|
|
|
---
|
|
|
|
## Phase 4: User management (P2)
|
|
|
|
| Task | Detalhe |
|
|
|------|---------|
|
|
| 4.1 | `GET /api/v1/auth/users` — super_admin |
|
|
| 4.2 | `PATCH /api/v1/auth/users/{username}` — role, active |
|
|
| 4.3 | UI mínima em view "Admin" (só super_admin) ou secção em Infra |
|
|
|
|
---
|
|
|
|
## Phase 5: Verification & deploy
|
|
|
|
| Task | Detalhe |
|
|
|------|---------|
|
|
| 5.1 | `scripts/verify-auth.sh` — 20+ asserts |
|
|
| 5.2 | Confirmar webhooks 001/002 ainda passam |
|
|
| 5.3 | `docker-compose up -d --build api frontend` |
|
|
| 5.4 | Testar público: `curl api.ops.ligbox.com.br/.../tickets` → 401 |
|
|
| 5.5 | Rotacionar `JWT_SECRET` e `DESK_BOOTSTRAP_PASSWORD` em produção |
|
|
| 5.6 | Documentar no BACKLOG.md: 003 ✅ |
|
|
|
|
---
|
|
|
|
## Risk & Mitigation
|
|
|
|
| Risco | Mitigação |
|
|
|-------|-----------|
|
|
| Worker audit quebra | `OPS_INTERNAL_TOKEN` no worker `.env` |
|
|
| Traefik healthcheck falha | `/health` permanece público |
|
|
| Lockout super_admin | Seed via env + SSH sqlite fallback documentado |
|
|
| Senha bootstrap fraca | Warning no startup + quickstart rotação |
|
|
|
|
---
|
|
|
|
## Rollback
|
|
|
|
1. `DESK_AUTH_ENABLED=false` no `.env` → feature flag bypass (implementar no plan)
|
|
2. Rebuild API sem dependencies auth
|
|
3. Revert frontend para `app.js` anterior
|
|
|
|
Feature flag `DESK_AUTH_ENABLED` (default `true` após deploy): quando `false`, API comporta-se como hoje (só para emergência).
|
|
|
|
---
|
|
|
|
## Version bump
|
|
|
|
API version: `0.6.0-desk-auth`
|