Sécurisation des échanges gRPC via les intercepteurs et protoc-gen-validate

Validation sémantique des microservices avec Protocol Buffers

Dans les architectures distribuées, l'échange de données structurées est fondamental. Si Protocol Buffers (Protobuf) garantit la sûreté des types, il n'impose pas de règles sémantiques sur les valeurs. L'outil protoc-gen-validate (PGV) comble cette lacune en générant du code de validation directement à partir des fichiers .proto, assurant ainsi l'intégrité des données avant même qu'elles n'atteignent la logique métier.

Avantages de la validation à la compilation

  • Indépendance du langage : Génération de validateurs pour Go, Java, C++, et bien d'autres.
  • Performence optimale : Les contrôles sont compilés, éliminant la réflexion coûteuse à l'exécution.
  • Richesse des contraintes : Support des plages numériques, formats de chaînes, expressions régulières et dépendances entre messages.

Définition des contraintes dans les fichiers Proto

Pour illustrer le fonctionnement, prenons l'exemple d'un profil utilisateur et de ses coordonnées géographiques. Voici comment annoter le schéma Protobuf :

syntax = "proto3";

package user_management;

import "validate/validate.proto";

message UserProfile {
  uint64 user_id = 1 [(validate.rules).uint64.gt = 10000];
  
  string contact_email = 2 [(validate.rules).string.email = true];
  
  string display_name = 3 [(validate.rules).string = {
    pattern:   "^[A-Za-zÀ-ÿ\\s'-]+$",
    max_bytes: 128,
  }];
  
  GeographicCoordinate primary_location = 4 [(validate.rules).message.required = true];
}

message GeographicCoordinate {
  double latitude = 1 [(validate.rules).double = {gte: -90.0,  lte: 90.0}];
  double longitude = 2 [(validate.rules).double = {gte: -180.0, lte: 180.0}];
}

La génération du code de validation s'effectue via le compilateur protoc :

protoc \
  -I . \
  -I ./third_party/validate/ \
  --go_out=paths=source_relative:./gen \
  --validate_out=lang=go,paths=source_relative:./gen \
  user_profile.proto

Intégration transparente via les intercepteurs gRPC

Les intercepteurs gRPC agissent comme des middleware. En les configurant correctement, on peut automatiser la validation des requêtes entrantes et sortantes sans polluer le code métier.

Implémentation en Go

En Go, l'intercepteur unaire peut vérifier dynamiquement si le message implémente l'interface de validation générée.

func validationUnaryInterceptor(
    ctx context.Context, 
    payload interface{}, 
    info *grpc.UnaryServerInfo, 
    handler grpc.UnaryHandler,
) (interface{}, error) {
    
    if validatable, isValid := payload.(interface{ Validate() error }); isValid {
        if validationErr := validatable.Validate(); validationErr != nil {
            return nil, status.Errorf(
                codes.InvalidArgument, 
                "données entrantes invalides : %v", validationErr,
            )
        }
    }
    
    return handler(ctx, payload)
}

// Initialisation du serveur
func startGrpcServer() {
    srv := grpc.NewServer(
        grpc.UnaryInterceptor(validationUnaryInterceptor),
    )
    // Enregistrement des services...
}

Implémentation en Java

Pour l'écosystème Java, la bibliothèque officielle fournit des intercepteurs prêts à l'emploi qui s'appuient sur un index de validateurs.

<dependency>
    <groupId>build.buf.protoc-gen-validate</groupId>
    <artifactId>pgv-java-grpc</artifactId>
    <version>${pgv.version}</version>
</dependency>

// Configuration de l'index de validation
ValidatorIndex validatorRegistry = new ReflectiveValidatorIndex();

// Application côté client
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051).build();
UserServiceGrpc.UserServiceBlockingStub client = UserServiceGrpc.newBlockingStub(channel)
    .withInterceptors(new ValidatingClientInterceptor(validatorRegistry));

// Application côté serveur
Server grpcServer = ServerBuilder.forPort(50051)
    .addService(ServerInterceptors.intercept(
        new UserServiceImpl(), 
        new ValidatingServerInterceptor(validatorRegistry)
    ))
    .build();

Extensions et règles métier complexes

Les règles natives de PGV couvrent la majorité des cas, mais certaines logiques métier nécessitent des validateurs personnalisés. En Java, il est possible d'implémenter l'interface Validator pour des contrôles transversaux.

public class InventoryValidator implements Validator<StockUpdateRequest> {
    @Override
    public void assertValid(StockUpdateRequest request) throws ValidationException {
        if (request.getAction() == StockAction.DECREASE && request.getQuantity() <= 0) {
            throw new ValidationException("Une réduction de stock exige une quantité strictement positive.");
        }
        if (request.getWarehouseId().isEmpty() && request.isGlobalSync()) {
            throw new ValidationException("L'identifiant de l'entrepôt est requis pour une synchronisation globale.");
        }
    }
}

Optimisation et bonnes pratiques

  • Validation précoce : Placez les intercepteurs au plus près de la couche réseau pour rejeter immédiatement les requêtes malformées.
  • Gestion des erreurs : Retournez des codes d'erreur gRPC précis (comme INVALID_ARGUMENT) accompagnés de détails structurés pour faciliter le débogage côté client.
  • Champs optionnels : Utilisez la directive ignore_empty pour éviter de valider des champs qui n'ont pas été renseignés, réduisant ainsi les faux positifs.
  • Éviter les cycles : Ne combinez pas la règle required avec des messages qui se référencent mutuellement, sous peine de créer des boucles infinies lors de la validation.

Évolution vers protovalidate

Il est crucial de noter que protoc-gen-validate est désormais en mode maintenance. L'équipe de Buf a introduit protovalidate, son successeur spirituel. Ce nouveau framework offre une architecture basée sur le CEL (Common Expression Language), permettant des expressions de validation beaucoup plus puissantes et une meilleure cohérence entre les différents langages cibles. Pour toute nouvelle implémentation, l'adoption directe de protovalidate est fortement recommandée, tandis que les systèmes existants peuvent planifier une migration progressive.

Étiquettes: grpc Protobuf protoc-gen-validate Protovalidate Microservices

Publié le 22 octobre à 19h15