# Account Lockout (Brute-Force zaštita) — A12

# Bilko — Account Lockout (Brute-Force zaštita) — A12 / MC #104687

**Status:** implementirano 2026-07-03, commit `13b3a729` na `feat/task-104687` (Wave 1 — čeka merge u dev)
**Vlasnik:** CodeCraft backend · Proveo PASS (nezavisna verifikacija svih 5 AC)

## Ponašanje

- Nakon **5 uzastopnih neuspješnih** email/password login pokušaja, nalog se zaključava **15 minuta**.
- **6. pokušaj vraća HTTP 423** (`BILKO-AUTH-005`) **čak i s tačnom šifrom** — provjera zaključanosti ide PRIJE verifikacije šifre.
- Uspješan login **resetuje brojač** na 0 (legitimni korisnici s povremenim tipfelerima se ne zaključavaju).
- Istekli `locked_until` se auto-čisti prije provjere šifre (cooldown se sam otključa).
- **Admin unlock:** `PUT /api/v1/users/{id}/unlock` (permisija `users:manage`, org-scoped — cross-org pokušaj vraća 404).
- **Entra-only korisnici** (`passwordHash = NULL`, V66) nikad ne dotiču lockout logiku; Entra SSO put (`markEntraLogin`) ne konsultuje lockout.

## Implementacija

| Sloj | Šta |
|---|---|
| Migracija | `V108__account_lockout.sql` — `users.failed_login_attempts INT NOT NULL DEFAULT 0`, `users.locked_until TIMESTAMPTZ NULL` (aditivno) + drop/recreate 3 `bilko_auth.*` SECURITY DEFINER lookup funkcije (RETURNS TABLE promjena; OWNER/GRANT identični V31/V66 originalima — diffano) |
| Servis | `AuthService.kt` — lock-check prije `verifyPassword`; brojač = jedan atomski parametrizovani UPDATE (nema izgubljenih inkremenata pod konkurencijom); `adminUnlock(orgId, userId)` |
| HTTP | `LockedException` → `BILKO-AUTH-005` (423) u `StatusPages.kt`; ruta unlock u `UserManagementRoutes.kt`; dokumentovano u `ERROR-CODES.md` + `openapi.yaml` |
| Testovi | `AuthServiceLockoutTest.kt` — 5 scenarija (6. pokušaj s tačnom šifrom → 423; reset na uspjeh; cooldown expiry; admin unlock; cross-org 404). Izvršeno 5/5 + regresija `AuthServiceTest` 31/31, skipped=0 (XML ground truth) |

## Bitno: javna login ruta je 410

`POST /api/v1/auth/login` vraća **410 Gone** platformski (WP4/MC #103144 — Bilko je Entra-only, password login penzionisan). Lockout je time defense-in-depth: javna brute-force površina je ZATVORENA (Proveo live probe: 7×410 na api.bilko.cloud), a lockout se automatski primjenjuje ako se ruta ikad reaktivira. Odluka: rutu NE reaktivirati radi live-423 demonstracije.

## Poznati follow-up

- `bilko_auth.find_user_by_email` / `find_user_by_id` i dalje nemaju `SET search_path = public, pg_temp` (naslijeđeno iz V31, nije uvedeno ovim taskom) — hardening ticket.

## Evidence

- `/Users/makinja/system/evidence/104687/build-report-20260703.md`
- `/Users/makinja/system/evidence/104687/proveo-verdict-20260703.md`
- P2P mesh: `mesh-thr-357b1180` (builder) → `mesh-thr-516f2df0` / `mesh-msg-ba98cc59` (Proveo PASS)

## Ops napomene (otkriveno tokom taska)

1. **Gitleaks pre-commit** je gađao `../../.gitleaks.toml` — rušio SVAKI commit iz nested `.claude/worktrees/*` (fail-closed "Secrets detected" bez skena). Fixano u `.husky/pre-commit` (commit `13b3a729`): config je tracked na repo root-u → `--show-toplevel/.gitleaks.toml`.
2. **Testcontainers silent-skip:** `~/.testcontainers.properties` je pokazivao na legacy `docker.raw.sock` koji Ryuk ne može bind-mountati na aktuelnom Docker Desktopu → SVI integration testovi tiho SKIPPED uz "BUILD SUCCESSFUL". Fix: `unix:///var/run/docker.sock` + `ryuk.disabled=true`. **Lekcija: uvijek čitati `tests=`/`skipped=` iz testsuite XML-a, ne vjerovati zelenom buildu.**