# 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`
