obsidian-vault/ligbox-ops-platform/specs/003-desk-auth-rbac/plan.md
2026-06-19 17:26:42 +00:00

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:

  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):

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.jsgetToken(), 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