9.5 KiB
Implementation Plan: Desk Auth & RBAC (003)
Branch: 003-desk-auth-rbac | Date: 2026-06-10 | Spec: 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):
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
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)
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
┌─────────────────────────────────────┐
│ 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
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
# 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:
- API 100% aberta hoje — confirmado em
api.ops.ligbox.com.br - JWT em header (não cookie) — mais simples com SPA estática nginx
- Worker audit chama
POST /api/v1/audit/cycle— usarOPS_INTERNAL_TOKENheader interno - Traefik pode proxy
/apino 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):
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/healthPOST /api/v1/auth/loginPOST /api/v1/webhooks/onboardPOST /api/v1/webhooks/ingress/{integration}
Worker bypass:
POST /api/v1/audit/cycleaceitaX-Ops-Internal-Token==OPS_INTERNAL_TOKEN
Implementação:
- Refactor:
APIRoutercomdependencies=[Depends(get_current_user)]por grupo _enrich_ticket(): chamar_mask_for_role(ticket, user.role)se noc- Migration tickets:
ALTER TABLEou recreate columnassigned_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
DESK_AUTH_ENABLED=falseno.env→ feature flag bypass (implementar no plan)- Rebuild API sem dependencies auth
- Revert frontend para
app.jsanterior
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