Introduzione
Quando un’applicazione cresce, non è sufficiente verificare soltanto che un utente sia autenticato. Spesso è necessario stabilire quali operazioni può eseguire: leggere un report, modificare un ordine, eliminare un account o accedere a una funzione amministrativa.
Un modo semplice e riutilizzabile per gestire queste regole consiste nell’utilizzare i decoratori. Un decoratore può controllare i permessi prima di eseguire una funzione, senza modificare il codice interno della funzione stessa.
In questo tutorial costruiremo un piccolo sistema di autorizzazione basato su:
- un oggetto che rappresenta l’utente corrente;
- un decoratore per richiedere uno o più permessi;
- la metaprogrammazione tramite gli attributi della funzione;
- un controllo centralizzato e facilmente estendibile.
Il risultato sarà utile in API, applicazioni web, pannelli amministrativi e servizi interni.
Codice completo
from dataclasses import dataclass
from functools import wraps
from typing import Callable, Any
class AuthorizationError(Exception):
"""Eccezione sollevata quando mancano i permessi necessari."""
@dataclass(frozen=True)
class User:
username: str
permissions: frozenset[str]
def has_permissions(self, required: set[str]) -> bool:
"""Restituisce True se l´utente possiede tutti i permessi richiesti."""
return required.issubset(self.permissions)
# In un´applicazione reale potrebbe essere gestito da una richiesta HTTP,
# da una sessione o da un sistema di autenticazione.
current_user = User(
username="marta",
permissions=frozenset({"report:read", "invoice:read"})
)
def require_permissions(*permissions: str) -> Callable:
"""
Decoratore parametrizzato che autorizza una funzione
solo se l´utente corrente possiede tutti i permessi indicati.
"""
required = set(permissions)
def decorator(function: Callable) -> Callable:
@wraps(function)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if not current_user.has_permissions(required):
missing = required - current_user.permissions
raise AuthorizationError(
f"Accesso negato a ´{function.__name__}´. "
f"Permessi mancanti: {´, ´.join(sorted(missing))}"
)
return function(*args, **kwargs)
# Metadati personalizzati: la funzione conserva
# l´informazione sui permessi richiesti.
wrapper.required_permissions = frozenset(required)
return wrapper
return decorator
@require_permissions("report:read")
def read_report(report_id: int) -> str:
return f"Lettura del report {report_id}"
@require_permissions("invoice:read", "invoice:delete")
def delete_invoice(invoice_id: int) -> str:
return f"Fattura {invoice_id} eliminata"
def describe_permissions(function: Callable) -> None:
"""
Ispeziona una funzione decorata usando metadati aggiunti
dinamicamente dal decoratore.
"""
permissions = getattr(function, "required_permissions", set())
print(f"Funzione: {function.__name__}")
print(f"Permessi richiesti: {´, ´.join(sorted(permissions))}")
if __name__ == "__main__":
describe_permissions(read_report)
print(read_report(42))
try:
print(delete_invoice(99))
except AuthorizationError as error:
print(error) Spiegazione
Il modello dell’utente
La classe User contiene il nome dell’utente e un insieme immutabile di permessi. Usiamo frozenset perché i permessi non dovrebbero essere modificati casualmente durante l’esecuzione di una richiesta.
Il metodo has_permissions usa issubset. Se una funzione richiede invoice:read e invoice:delete, entrambi devono essere presenti nell’insieme dell’utente.
Il decoratore parametrizzato
require_permissions riceve un numero variabile di stringhe. Questa struttura crea tre livelli:
- la funzione esterna riceve i permessi;
- la funzione
decoratorriceve la funzione da decorare; - la funzione
wrapperintercetta la chiamata reale.
Prima di eseguire la funzione originale, il wrapper confronta i permessi richiesti con quelli dell’utente. Se manca anche un solo permesso, viene sollevata AuthorizationError.
@wraps(function) è importante perché copia metadati come il nome e la documentazione della funzione originale. Senza di esso, strumenti di introspezione e messaggi di errore vedrebbero soltanto il nome wrapper.
La parte di metaprogrammazione
Il decoratore aggiunge dinamicamente l’attributo required_permissions alla funzione decorata. Questo è un esempio semplice di metaprogrammazione: il programma modifica o arricchisce il comportamento e la struttura degli oggetti durante l’esecuzione.
La funzione describe_permissions usa getattr per leggere questo attributo. In questo modo possiamo costruire documentazione automatica, controlli amministrativi o strumenti che mostrano i requisiti di sicurezza delle funzioni.
Nel programma, read_report funziona perché l’utente possiede report:read. Invece delete_invoice fallisce perché mancano almeno invoice:delete, anche se l’utente possiede invoice:read.
Best practice
- Centralizza i controlli: il decoratore evita di duplicare verifiche in ogni funzione protetta.
- Usa nomi coerenti: convenzioni come
resource:actionrendono i permessi leggibili e facilmente filtrabili. - Preserva i metadati: usa sempre
functools.wrapsnei decoratori che sostituiscono una funzione. - Non confondere autenticazione e autorizzazione: l’autenticazione identifica l’utente; il decoratore verifica cosa può fare.
- Non affidarti soltanto all’interfaccia: i controlli devono essere eseguiti nel livello che effettivamente compie l’operazione.
- Testa i casi negativi: verifica che una funzione venga bloccata quando manca un permesso e che l’errore sia comprensibile.
- Evita lo stato globale nei sistemi concorrenti: in un web server è preferibile passare l’utente tramite una richiesta, un contesto o una dependency injection.
Una possibile estensione consiste nel supportare anche la modalità “almeno uno tra i permessi”, oppure nel consentire permessi gerarchici, ad esempio trattando admin come autorizzazione superiore a tutte le altre.
Riepilogo
I decoratori permettono di applicare regole di autorizzazione in modo dichiarativo: basta aggiungere @require_permissions alla funzione da proteggere. La logica rimane separata dal dominio applicativo, rendendo il codice più leggibile e più semplice da manutenere.
L’uso di attributi aggiunti dinamicamente dimostra inoltre come i decoratori possano diventare strumenti di metaprogrammazione. Non si limitano a eseguire controlli: possono descrivere le funzioni, fornire informazioni ad altri componenti e alimentare sistemi automatici di documentazione o analisi.
