Decoratori e metaprogrammazione in Python: costruire un sistema di autorizzazione

by theArchitect
SHARE
Decoratori e metaprogrammazione in Python: costruire un sistema di autorizzazione
© Guida-HTML5.it

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 decorator riceve la funzione da decorare;
  • la funzione wrapper intercetta 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:action rendono i permessi leggibili e facilmente filtrabili.
  • Preserva i metadati: usa sempre functools.wraps nei 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.

Approfondisci con risorse ufficiali

SHARE