Estratègia de Protecció de Dades i Autorització
Tipus: Document d'Arquitectura de Seguretat
Versió: 1.0
Actualitzat: 2024-12
Estat: Normatiu (defineix estàndards a seguir)
Referència: Veure també Security Playbook
Resum Executiu
Aquest document defineix l'estratègia de protecció de dades i autorització per a la Line Dance Platform (LDP). Complementa el document authentication.md (autenticació JWT) amb els controls necessaris per:
- Minimitzar l'exposició de dades (principi de mínim privilegi)
- Garantir autorització a nivell d'objecte (prevenció BOLA/IDOR)
- Protegir dades en trànsit i en repòs
- Establir controls de detecció i traçabilitat
El document segueix les recomanacions de OWASP API Security Top 10 (2023) i bones pràctiques de la indústria.
Índex
- Principi Fonamental
- Capes de Defensa
- Minimització de Dades (Least Data)
- Autorització a Nivell d'Objecte (BOLA)
- Model de Permisos RBAC/Ownership
- Protecció en Trànsit
- Protecció en Repòs (At Rest)
- Controls de Detecció i Reducció de Dany
- Estat Actual i Roadmap de Seguretat
- Patrons d'Implementació
- Checklist de Seguretat per Endpoint
- Referències
1. Principi Fonamental
Tot el que s'envia al client (web o app) s'ha de considerar "exfiltrable".
Un usuari amb coneixements pot inspeccionar la xarxa, llegir la memòria, capturar JSON, o accedir a fitxers locals. Per això, la protecció real es basa en:
- No enviar dades innecessàries (minimització)
- Aplicar autorització al servidor (no al frontend)
El frontend pot aplicar controls d'UX (amagar botons, filtrar vistes), però mai s'ha de confiar en ell per a seguretat real.
2. Capes de Defensa
L'estratègia segueix un model de defensa en profunditat amb múltiples capes:
┌─────────────────────────────────────────────────────────────┐
│ CLIENT (Web/Mòbil) │
│ - UX controls (amagar elements) │
│ - Validació de formularis │
│ - TLS certificate pinning (mòbil) │
└─────────────────────────┬───────────────────────────────────┘
│ HTTPS/TLS
▼
┌─────────────────────────────────────────────────────────────┐
│ API GATEWAY / EDGE │
│ - Rate limiting │
│ - WAF (Web Application Firewall) │
│ - Request validation │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SPRING SECURITY FILTER │
│ - Autenticació JWT │
│ - Validació de token │
│ - Extracció de principal (User) │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CONTROLLER LAYER (@PreAuthorize) │
│ - Validació de rol │
│ - Validació d'input │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SERVICE LAYER (Autorització d'Objecte) │ ◄── CAPA CRÍTICA
│ - Validació de propietat (ownership) │
│ - Validació d'estat del recurs │
│ - Business rules │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ REPOSITORY LAYER │
│ - Accés a dades │
│ - (Opcional) Row-level security │
└─────────────────────────────────────────────────────────────┘
La capa crítica és el SERVICE LAYER, on s'ha de validar que l'usuari autenticat té permís sobre l'objecte específic que vol accedir/modificar.
3. Minimització de Dades (Least Data)
3.1. Principi
"No enviïs dades que el client no necessita per a la funcionalitat actual."
La minimització redueix el "blast radius" si algú captura respostes de l'API.
3.2. Estratègies de DTOs
DTOs per Cas d'Ús
Definir vistes diferents segons el context:
| Context | DTO | Camps |
|---|---|---|
| Llista | DanceSummaryDto | id, name, level, choreographers |
| Detall públic | DanceDetailDto | + description, songs, links |
| Edició (owner) | DanceEditDto | + owner info, timestamps |
| Admin | DanceAdminDto | + audit fields, internal notes |
DTOs per Rol (Opcional)
Si el mateix endpoint serveix diferents rols, el mapper pot aplicar projeccions:
public DanceDto toDto(Dance entity, User viewer) {
DanceDto dto = basicProjection(entity);
if (isOwnerOrAdmin(entity, viewer)) {
dto.setOwnerEmail(entity.getOwner().getEmail());
dto.setInternalNotes(entity.getInternalNotes());
}
return dto;
}
3.3. Paginació Obligatòria
Tots els endpoints de llista han d'implementar paginació server-side:
@GetMapping
public PageResponse<DanceSummaryDto> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size // Màxim 100
) {
int safeSize = Math.min(size, 100); // Límit dur
// ...
}
Regles:
- Màxim 100 elements per pàgina
- El servidor sempre decideix el subconjunt
- No acceptar
?limit=999999
3.4. Camps Sensibles - Política
| Camp | Visibilitat | Justificació |
|---|---|---|
user.email | Només owner/admin | PII |
user.passwordHash | MAI | Credencial |
*.ownerId | Intern (no exposar) | Evita IDOR |
*.createdBy.email | Només admin | Auditoria |
ownershipRequest.message | Admin + requester | Privat |
Si dubtes si un camp ha de ser públic, no l'incloguis al DTO públic.
4. Autorització a Nivell d'Objecte (BOLA)
4.1. Què és BOLA?
Broken Object Level Authorization (BOLA) és el risc #1 a OWASP API Security 2023. Es produeix quan:
- L'API rep un identificador (
/api/dances/123) - L'API retorna/modifica l'objecte sense verificar que l'usuari té permís sobre aquest objecte específic
Exemple vulnerable:
// ❌ VULNERABLE: només comprova autenticació, no autorització
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public SecretDto get(@PathVariable Long id) {
return repo.findById(id); // Qualsevol usuari pot accedir a qualsevol ID
}
Exemple segur:
// ✅ SEGUR: comprova propietat
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public SecretDto get(@PathVariable Long id, @AuthenticationPrincipal User user) {
Secret entity = repo.findById(id).orElseThrow();
if (!canAccess(entity, user)) {
throw new AccessDeniedException("No tens accés a aquest recurs");
}
return mapper.toDto(entity);
}
4.2. On Aplicar Validació BOLA
| Operació | Requereix BOLA? | Exemple |
|---|---|---|
| GET by ID | ⚠️ Depèn | Si és dada sensible, SÍ |
| PUT/PATCH | ✅ SEMPRE | Només owner/admin pot modificar |
| DELETE | ✅ SEMPRE | Només owner/admin pot eliminar |
| POST nested | ✅ SEMPRE | Verificar permís sobre el parent |
| GET list | ⚠️ Depèn | Filtrar per ownership si cal |
4.3. Patró de Validació Estàndard
Tots els serveis amb recursos "owned" han d'implementar aquest patró:
@Service
public class ResourceService {
/**
* Determina si l'usuari pot accedir/modificar el recurs.
* Centralitzat per consistència.
*/
public boolean canAccess(Resource entity, User user, AccessType type) {
if (user == null) return false;
// Admin sempre pot
if (user.getRole() == Role.ADMIN) return true;
// Owner pot segons tipus d'accés i estat
if (isOwner(entity, user)) {
return switch (type) {
case READ -> true;
case WRITE -> entity.isEditable(); // Depèn d'estat
case DELETE -> entity.isDeletable();
};
}
return false;
}
private boolean isOwner(Resource entity, User user) {
return entity.getOwner() != null
&& entity.getOwner().getId().equals(user.getId());
}
}
5. Model de Permisos RBAC/Ownership
5.1. Rols del Sistema (RBAC)
| Rol | Descripció | Permisos Globals |
|---|---|---|
USER | Usuari registrat | Llegir públic, gestionar perfil propi |
TEACHER | Professor verificat | Crear/editar contingut propi |
ADMIN | Administrador | Tot |
5.2. Ownership (Propietat)
Més enllà dels rols, apliquem ownership per recursos:
┌──────────────────────────────────────────────────────────┐
│ ADMIN │
│ - Pot accedir/modificar qualsevol recurs │
│ - Pot canviar ownership │
│ - Pot veure audit logs │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ ROL (TEACHER/USER) │
│ - Determina què POT crear │
│ - No determina què pot modificar/eliminar │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ OWNERSHIP │
│ - Determina qui pot MODIFICAR/ELIMINAR │
│ - Resource.owner == User.id │
│ - Pot dependre de l'estat del recurs │
└──────────────────────────────────────────────────────────┘
5.3. Matriu de Permisos per Entitat
| Entitat | Create | Read (públic) | Read (privat) | Update | Delete |
|---|---|---|---|---|---|
| Dance | TEACHER+ | ✅ Tots | Owner/Admin | Owner/Admin | Admin |
| Song | TEACHER+ | ✅ Tots | Owner/Admin | Owner/Admin | Admin |
| Choreographer | AUTH | ✅ Tots | Owner/Admin | Owner (si CLAIMED) | Admin |
| Event | TEACHER+ | ✅ Tots | - | Owner/Admin | Owner/Admin |
| Venue | TEACHER+ | ✅ Tots | - | Owner/Admin | Admin |
| Location | TEACHER+ | ✅ Tots | - | Admin | Admin |
| UserProfile | - | - | Self only | Self only | - |
| OwnershipRequest | AUTH | Admin | Self/Admin | - | Admin |
5.4. Estats i Transicions (Choreographer com a exemple)
L'ownership pot dependre de l'estat del recurs:
INFORMATIVE ──────► PENDING_CLAIM ──────► CLAIMED
│ │ │
│ (qualsevol pot │ (admin revisa) │ (owner pot editar)
│ sol·licitar) │ │
│ ▼ ▼
│ REJECTED PENDING_TRANSFER
│ │
│ ▼
│ CLAIMED (nou owner)
Regles d'edició per estat:
INFORMATIVE: Només adminPENDING_CLAIM: Només adminCLAIMED: Owner (camps limitats) + Admin (tot)PENDING_TRANSFER: Només admin
6. Protecció en Trànsit
6.1. TLS Obligatori
- Producció: HTTPS obligatori per a totes les comunicacions
- Desenvolupament: HTTP permès només per comoditat local
- Certificats: Renovació automàtica (Let's Encrypt o similar)
6.2. Headers de Seguretat
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
6.3. Tokens i Sessions
| Token | Durada | Renovació | Revocació |
|---|---|---|---|
| Access Token | 15 min | Via refresh | tokenVersion++ |
| Refresh Token | 7 dies | Automàtica | tokenVersion++ |
Revocació global: Incrementar user.tokenVersion invalida tots els tokens emesos.
6.4. Certificate Pinning (Mòbil)
Per a l'app Android, implementar certificate pinning per elevar el cost d'intercepció MITM:
// OkHttp certificate pinning
val certificatePinner = CertificatePinner.Builder()
.add("api.linedance.example", "sha256/XXXX...")
.build()
El pinning no és infal·lible (es pot bypassejar amb root/Frida), però eleva significativament el cost d'atac.
7. Protecció en Repòs (At Rest)
7.1. Dades al Servidor
| Tipus | Protecció | Implementació |
|---|---|---|
| Passwords | Hash BCrypt (cost 12) | Ja implementat |
| Tokens | No es guarden (stateless JWT) | Ja implementat |
| PII | Xifrat a BD (opcional) | Roadmap |
| Backups | Xifrat AES-256 | Infraestructura |
7.2. Dades al Client Web
El navegador és un entorn hostil. Qualsevol dada que hi arribi és llegible per l'usuari.
Estratègia web:
- Minimització (no enviar el que no cal)
- Cache temporal i mínima
- No guardar dades sensibles a localStorage/IndexedDB
- El xifrat local només protegeix contra pèrdua del dispositiu, no contra l'usuari actiu
7.3. Dades al Client Mòbil (Android)
Per a dades offline, aplicar:
┌─────────────────────────────────────────────────┐
│ Envelope Encryption │
├─────────────────────────────────────────────────┤
│ │
│ Dades ──► Xifrat amb DEK ──► Dades Xifrades │
│ │ │
│ ▼ │
│ DEK (Data Encryption Key) │
│ │ │
│ ▼ │
│ KEK (Key Encryption Key) │
│ ↓ │
│ Android Keystore (hardware-backed) │
│ │
└─────────────────────────────────────────────────┘
Implementació recomanada:
- BD local: SQLCipher per SQLite xifrat
- Claus: Android Keystore amb claus no exportables
- Desbloqueig: Requerir biometria/PIN per accedir a dades sensibles
8. Controls de Detecció i Reducció de Dany
8.1. Rate Limiting
Limitar peticions per evitar "data dumping":
| Endpoint | Límit | Finestra |
|---|---|---|
| Login | 5 intents | 15 min |
| API general | 100 req | 1 min |
| Search | 30 req | 1 min |
| Export | 5 req | 1 hora |
8.2. Auditoria d'Accés
Registrar accessos a recursos sensibles:
@Slf4j
public class AuditService {
public void logAccess(User user, String resourceType, Long resourceId, String action) {
log.info("AUDIT: user={} action={} resource={}:{}",
user.getEmail(), action, resourceType, resourceId);
// Guardar a BD d'auditoria per anàlisi posterior
}
}
Events a auditar:
- Accessos a dades d'altres usuaris (per admin)
- Modificacions de permisos/ownership
- Intents d'accés denegats
- Operacions de massa (exports, bulk updates)
8.3. Detecció d'Anomalies (Roadmap)
- Accessos des de noves IPs/devices
- Patrons de scraping (massa peticions seqüencials)
- Intents massius de IDs (
/api/dances/1,/api/dances/2, ...)
9. Estat Actual i Roadmap de Seguretat
9.1. Estat per Entitat
| Entitat | Auth | BOLA | Ownership | DTOs | Tests BOLA |
|---|---|---|---|---|---|
| Choreographer | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dance | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Song | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Event | ✅ | ❌ | ❌ | ⚠️ | ❌ |
| Venue | ⚠️ | ❌ | ❌ | ⚠️ | ❌ |
| Location | ⚠️ | ❌ | ❌ | ⚠️ | ❌ |
| UserProfile | ✅ | ✅ | ✅ | ✅ | ❌ |
Llegenda:
- ✅ Implementat correctament
- ⚠️ Parcial o necessita revisió
- ❌ No implementat
9.2. Roadmap de Millores
Fase 1: Correccions Crítiques (P0)
| Tasca | Entitat | Descripció |
|---|---|---|
| SEC-001 | Location | Afegir @PreAuthorize a tots els endpoints |
| SEC-002 | Venue | Afegir @PreAuthorize a tots els endpoints |
| SEC-003 | Event | Afegir camp createdBy i validació ownership |
| SEC-004 | Event | Implementar canEdit() al servei |
Fase 2: Millores de Seguretat (P1)
| Tasca | Descripció |
|---|---|
| SEC-010 | Crear tests BOLA per cada endpoint amb ID |
| SEC-011 | Revisar DTOs: separar públics/privats |
| SEC-012 | Implementar rate limiting a nivell d'aplicació |
| SEC-013 | Afegir logging d'auditoria |
Fase 3: Seguretat Avançada (P2)
| Tasca | Descripció |
|---|---|
| SEC-020 | Xifrat de camps PII a BD |
| SEC-021 | Certificate pinning a app Android |
| SEC-022 | SQLCipher per cache offline |
| SEC-023 | Detecció d'anomalies bàsica |
10. Patrons d'Implementació
10.1. Patró: Servei amb Validació BOLA
@Service
@RequiredArgsConstructor
public class ExampleService {
private final ExampleRepository repo;
private final AuthUtil authUtil;
// ✅ READ: Validar accés si és dada sensible
public ExampleDto get(Long id) {
Example entity = repo.findById(id)
.orElseThrow(() -> new NotFoundException("Not found: " + id));
// Si conté dades sensibles, validar
User user = authUtil.getCurrentUser();
if (!canRead(entity, user)) {
throw new AccessDeniedException("No tens accés");
}
return mapper.toDto(entity, user); // DTO segons viewer
}
// ✅ UPDATE: SEMPRE validar ownership
@Transactional
public ExampleDto update(Long id, UpdateRequest request) {
Example entity = repo.findById(id)
.orElseThrow(() -> new NotFoundException("Not found: " + id));
User user = authUtil.getCurrentUser();
if (!canEdit(entity, user)) {
throw new AccessDeniedException("No pots editar aquest recurs");
}
// Aplicar canvis...
return mapper.toDto(repo.save(entity), user);
}
// ✅ DELETE: SEMPRE validar ownership
@Transactional
public void delete(Long id) {
Example entity = repo.findById(id)
.orElseThrow(() -> new NotFoundException("Not found: " + id));
User user = authUtil.getCurrentUser();
if (!canDelete(entity, user)) {
throw new AccessDeniedException("No pots eliminar aquest recurs");
}
repo.delete(entity);
}
// === Mètodes d'autorització centralitzats ===
private boolean canRead(Example entity, User user) {
if (entity.isPublic()) return true;
if (user == null) return false;
if (user.getRole() == Role.ADMIN) return true;
return isOwner(entity, user);
}
private boolean canEdit(Example entity, User user) {
if (user == null) return false;
if (user.getRole() == Role.ADMIN) return true;
return isOwner(entity, user) && entity.isEditable();
}
private boolean canDelete(Example entity, User user) {
if (user == null) return false;
if (user.getRole() == Role.ADMIN) return true;
return isOwner(entity, user) && entity.isDeletable();
}
private boolean isOwner(Example entity, User user) {
return entity.getOwner() != null
&& entity.getOwner().getId().equals(user.getId());
}
}
10.2. Patró: Controller amb @PreAuthorize
@RestController
@RequestMapping("/api/examples")
@RequiredArgsConstructor
public class ExampleController {
private final ExampleService service;
// Lectura pública
@GetMapping
public PageResponse<ExampleSummaryDto> list(Pageable pageable) {
return service.search(pageable);
}
// Lectura per ID (pot ser sensible)
@GetMapping("/{id}")
public ExampleDto get(@PathVariable Long id) {
return service.get(id); // Servei valida accés
}
// Creació: requereix rol
@PostMapping
@PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')")
public ExampleDto create(@Valid @RequestBody CreateRequest request) {
return service.create(request);
}
// Modificació: rol + ownership (validat al servei)
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')")
public ExampleDto update(
@PathVariable Long id,
@Valid @RequestBody UpdateRequest request
) {
return service.update(id, request); // Servei valida ownership
}
// Eliminació: normalment només admin
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
10.3. Patró: Test BOLA
@SpringBootTest
@AutoConfigureMockMvc
class ExampleControllerSecurityTest {
@Autowired MockMvc mockMvc;
@Autowired ExampleRepository repo;
@Autowired UserRepository userRepo;
@Test
@DisplayName("User cannot update resource owned by another user")
void update_byNonOwner_returns403() throws Exception {
// Given: Resource owned by User A
User ownerA = createUser("owner@test.com", Role.TEACHER);
Example resource = createResource(ownerA);
// When: User B (also TEACHER) tries to update
String tokenB = generateToken("other@test.com", Role.TEACHER);
mockMvc.perform(put("/api/examples/" + resource.getId())
.header("Authorization", "Bearer " + tokenB)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Hacked!"}
"""))
// Then: Should return 403 Forbidden
.andExpect(status().isForbidden());
}
@Test
@DisplayName("Owner can update their own resource")
void update_byOwner_succeeds() throws Exception {
// Given: Resource owned by User A
User owner = createUser("owner@test.com", Role.TEACHER);
Example resource = createResource(owner);
// When: Owner updates
String token = generateToken("owner@test.com", Role.TEACHER);
mockMvc.perform(put("/api/examples/" + resource.getId())
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Updated"}
"""))
// Then: Should succeed
.andExpect(status().isOk());
}
@Test
@DisplayName("Admin can update any resource")
void update_byAdmin_succeeds() throws Exception {
// Given: Resource owned by User A
User owner = createUser("owner@test.com", Role.TEACHER);
Example resource = createResource(owner);
// When: Admin updates
String adminToken = generateToken("admin@test.com", Role.ADMIN);
mockMvc.perform(put("/api/examples/" + resource.getId())
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Admin edit"}
"""))
// Then: Should succeed
.andExpect(status().isOk());
}
}
11. Checklist de Seguretat per Endpoint
Abans de considerar un endpoint "complet", verificar:
Autenticació
- Endpoint protegit té
@PreAuthorizeo regla a SecurityConfig - Endpoints públics estan documentats i justificats
Autorització (BOLA)
- Endpoints amb
{id}validen propietat al servei -
PUT/PATCHcomprovencanEdit(entity, user) -
DELETEcomprovacanDelete(entity, user) - Endpoints nested (
/parent/{id}/children) validen accés al parent
Minimització de Dades
- DTO no exposa camps innecessaris
- Camps sensibles només es retornen a qui correspon
- Llistats estan paginats amb límit màxim
Input Validation
- Request body validat amb
@Valid - IDs validats (positius, existents)
- Strings sanititzats si cal
Tests
- Test: accés correcte per owner
- Test: accés correcte per admin
- Test: accés denegat per no-owner
- Test: accés denegat per usuari no autenticat
12. Referències
OWASP
- OWASP API Security Top 10 (2023)
- API1:2023 - Broken Object Level Authorization
- Authorization Cheat Sheet
Spring Security
Android Security
Historial de Canvis
| Data | Versió | Canvis |
|---|---|---|
| 2024-12 | 1.0 | Document inicial |