Skip to main content

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:

  1. Minimitzar l'exposició de dades (principi de mínim privilegi)
  2. Garantir autorització a nivell d'objecte (prevenció BOLA/IDOR)
  3. Protegir dades en trànsit i en repòs
  4. 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

  1. Principi Fonamental
  2. Capes de Defensa
  3. Minimització de Dades (Least Data)
  4. Autorització a Nivell d'Objecte (BOLA)
  5. Model de Permisos RBAC/Ownership
  6. Protecció en Trànsit
  7. Protecció en Repòs (At Rest)
  8. Controls de Detecció i Reducció de Dany
  9. Estat Actual i Roadmap de Seguretat
  10. Patrons d'Implementació
  11. Checklist de Seguretat per Endpoint
  12. Referències

1. Principi Fonamental

Realitat Bàsica

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:

  1. No enviar dades innecessàries (minimització)
  2. 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:

ContextDTOCamps
LlistaDanceSummaryDtoid, name, level, choreographers
Detall públicDanceDetailDto+ description, songs, links
Edició (owner)DanceEditDto+ owner info, timestamps
AdminDanceAdminDto+ 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

CampVisibilitatJustificació
user.emailNomés owner/adminPII
user.passwordHashMAICredencial
*.ownerIdIntern (no exposar)Evita IDOR
*.createdBy.emailNomés adminAuditoria
ownershipRequest.messageAdmin + requesterPrivat
Regla d'Or

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:

  1. L'API rep un identificador (/api/dances/123)
  2. 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ènSi és dada sensible, SÍ
PUT/PATCH✅ SEMPRENomés owner/admin pot modificar
DELETE✅ SEMPRENomés owner/admin pot eliminar
POST nested✅ SEMPREVerificar permís sobre el parent
GET list⚠️ DepènFiltrar 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)

RolDescripcióPermisos Globals
USERUsuari registratLlegir públic, gestionar perfil propi
TEACHERProfessor verificatCrear/editar contingut propi
ADMINAdministradorTot

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

EntitatCreateRead (públic)Read (privat)UpdateDelete
DanceTEACHER+✅ TotsOwner/AdminOwner/AdminAdmin
SongTEACHER+✅ TotsOwner/AdminOwner/AdminAdmin
ChoreographerAUTH✅ TotsOwner/AdminOwner (si CLAIMED)Admin
EventTEACHER+✅ Tots-Owner/AdminOwner/Admin
VenueTEACHER+✅ Tots-Owner/AdminAdmin
LocationTEACHER+✅ Tots-AdminAdmin
UserProfile--Self onlySelf only-
OwnershipRequestAUTHAdminSelf/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 admin
  • PENDING_CLAIM: Només admin
  • CLAIMED: 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

TokenDuradaRenovacióRevocació
Access Token15 minVia refreshtokenVersion++
Refresh Token7 diesAutomàticatokenVersion++

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()
Limitació

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

TipusProteccióImplementació
PasswordsHash BCrypt (cost 12)Ja implementat
TokensNo es guarden (stateless JWT)Ja implementat
PIIXifrat a BD (opcional)Roadmap
BackupsXifrat AES-256Infraestructura

7.2. Dades al Client Web

Limitació 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":

EndpointLímitFinestra
Login5 intents15 min
API general100 req1 min
Search30 req1 min
Export5 req1 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

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

TascaEntitatDescripció
SEC-001LocationAfegir @PreAuthorize a tots els endpoints
SEC-002VenueAfegir @PreAuthorize a tots els endpoints
SEC-003EventAfegir camp createdBy i validació ownership
SEC-004EventImplementar canEdit() al servei

Fase 2: Millores de Seguretat (P1)

TascaDescripció
SEC-010Crear tests BOLA per cada endpoint amb ID
SEC-011Revisar DTOs: separar públics/privats
SEC-012Implementar rate limiting a nivell d'aplicació
SEC-013Afegir logging d'auditoria

Fase 3: Seguretat Avançada (P2)

TascaDescripció
SEC-020Xifrat de camps PII a BD
SEC-021Certificate pinning a app Android
SEC-022SQLCipher per cache offline
SEC-023Detecció 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é @PreAuthorize o regla a SecurityConfig
  • Endpoints públics estan documentats i justificats

Autorització (BOLA)

  • Endpoints amb {id} validen propietat al servei
  • PUT/PATCH comproven canEdit(entity, user)
  • DELETE comprova canDelete(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

Spring Security

Android Security


Historial de Canvis

DataVersióCanvis
2024-121.0Document inicial