github.com/argoproj/argo-cd/v3@v3.2.1/util/grpc/useragent.go (about)

     1  package grpc
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"strings"
     7  
     8  	"github.com/Masterminds/semver/v3"
     9  	"google.golang.org/grpc"
    10  	"google.golang.org/grpc/codes"
    11  	"google.golang.org/grpc/metadata"
    12  	"google.golang.org/grpc/status"
    13  )
    14  
    15  // parseSemVerConstraint returns a semVer Constraint instance or panic if there is a parsing error with the provided constraint.
    16  func parseSemVerConstraint(constraintStr string) *semver.Constraints {
    17  	semVerConstraint, err := semver.NewConstraint(constraintStr)
    18  	if err != nil {
    19  		panic(err)
    20  	}
    21  	return semVerConstraint
    22  }
    23  
    24  // UserAgentUnaryServerInterceptor returns a UnaryServerInterceptor which enforces a minimum client
    25  // version in the user agent
    26  func UserAgentUnaryServerInterceptor(clientName, constraintStr string) grpc.UnaryServerInterceptor {
    27  	semVerConstraint := parseSemVerConstraint(constraintStr)
    28  	return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
    29  		if err := userAgentEnforcer(ctx, clientName, semVerConstraint); err != nil {
    30  			return nil, err
    31  		}
    32  		return handler(ctx, req)
    33  	}
    34  }
    35  
    36  // UserAgentStreamServerInterceptor returns a StreamServerInterceptor which enforces a minimum client
    37  // version in the user agent
    38  func UserAgentStreamServerInterceptor(clientName, constraintStr string) grpc.StreamServerInterceptor {
    39  	semVerConstraint := parseSemVerConstraint(constraintStr)
    40  	return func(srv any, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    41  		if err := userAgentEnforcer(stream.Context(), clientName, semVerConstraint); err != nil {
    42  			return err
    43  		}
    44  		return handler(srv, stream)
    45  	}
    46  }
    47  
    48  func userAgentEnforcer(ctx context.Context, clientName string, semVerConstraint *semver.Constraints) error {
    49  	var userAgents []string
    50  	if md, ok := metadata.FromIncomingContext(ctx); ok {
    51  		for _, ua := range md["user-agent"] {
    52  			// ua is a string like "argocd-client/v0.11.0+cde040e grpc-go/1.15.0"
    53  			userAgents = append(userAgents, strings.Fields(ua)...)
    54  			break
    55  		}
    56  	}
    57  	if isLegacyClient(userAgents) {
    58  		return status.Errorf(codes.FailedPrecondition, "unsatisfied client version constraint: %s", semVerConstraint)
    59  	}
    60  
    61  	for _, userAgent := range userAgents {
    62  		uaSplit := strings.Split(userAgent, "/")
    63  		if len(uaSplit) != 2 || uaSplit[0] != clientName {
    64  			// User-agent was supplied, but client/format is not one we care about (e.g. grpc-go)
    65  			continue
    66  		}
    67  		// remove pre-release part
    68  		versionStr := strings.Split(uaSplit[1], "-")[0]
    69  		// We have matched the client name to the one we care about
    70  		uaVers, err := semver.NewVersion(versionStr)
    71  		if err != nil {
    72  			return status.Errorf(codes.InvalidArgument, "could not parse version from user-agent: %s", userAgent)
    73  		}
    74  		if ok, errs := semVerConstraint.Validate(uaVers); !ok {
    75  			return status.Errorf(codes.FailedPrecondition, "unsatisfied client version constraint: %s", errors.Join(errs...))
    76  		}
    77  		return nil
    78  	}
    79  	// If we get here, the caller either did not supply user-agent, supplied one which we don't
    80  	// care about. This implies it is a from a custom generated client, so we permit the request.
    81  	// We really only want to enforce user-agent version constraints for clients under our
    82  	// control which we know to have compatibility issues
    83  	return nil
    84  }
    85  
    86  // isLegacyClient checks if the request was made from a legacy Argo CD client (i.e. v0.10 CLI).
    87  // The heuristic is that a single default 'grpc-go' user-agent was specified with one of the
    88  // previous versions of grpc-go we used in the past (1.15.0, 1.10.0).
    89  // Starting in v0.11, both of the gRPC clients we maintain (pkg/apiclient and grpc-gateway) started
    90  // supplying a explicit user-agent tied to the Argo CD version.
    91  func isLegacyClient(userAgents []string) bool {
    92  	return len(userAgents) == 1 && (userAgents[0] == "grpc-go/1.15.0" || userAgents[0] == "grpc-go/1.10.0")
    93  }