# 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= JWT_EXPIRE_HOURS=8 DESK_AUTH_ENABLED=true OPS_INTERNAL_TOKEN= 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 │ ───────────────► │ + role check per endpoint │ ├─────────────────────────────────────┤ VM112 / Wazuh │ X-Webhook-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`