github.com/blend/go-sdk@v1.20220411.3/envoyutil/middleware.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package envoyutil
     9  
    10  import (
    11  	"context"
    12  	"net/http"
    13  
    14  	"github.com/blend/go-sdk/web"
    15  )
    16  
    17  // clientIdentityKey is an unexported sentinel type used to set and get the
    18  // client identity string on a `context`.
    19  type clientIdentityKey struct{}
    20  
    21  // WithClientIdentity adds the client identity to a context.
    22  //
    23  // For example if `rc` is a `*web.Ctx`
    24  // ```
    25  // rc.WithContext(envoyutil.WithClientIdentity(rc.Context(), clientIdentity))
    26  // ```
    27  func WithClientIdentity(ctx context.Context, clientIdentity string) context.Context {
    28  	return context.WithValue(ctx, clientIdentityKey{}, clientIdentity)
    29  }
    30  
    31  // GetClientIdentity returns the client identity of the calling service or
    32  // `""` if the client identity is unset.
    33  //
    34  // For example if `rc` is a `*web.Ctx`
    35  // ```
    36  // envoyutil.GetClientIdentity(rc.Context())
    37  // ```
    38  func GetClientIdentity(ctx context.Context) string {
    39  	value := ctx.Value(clientIdentityKey{})
    40  	asStr, _ := value.(string)
    41  	// If the type assertion fails, we still want to return the zero value for string.
    42  	return asStr
    43  }
    44  
    45  // ClientIdentityRequired produces a middleware function that determines the
    46  // client identity used in a connection secured with mTLS.
    47  //
    48  // This parses the `X-Forwarded-Client-Cert` (XFCC) from a request and uses
    49  // a client identity provider (`cip`, e.g. see `SPIFFEClientIdentityProvider()`) to
    50  // determine the client identity. Additionally, optional `verifiers` (e.g. see
    51  // `SPIFFEServerIdentityProvider()`) can be used to verify other parts of the XFCC
    52  // header such as the identity of the current server.
    53  //
    54  // In cases of error, the client identity will not be set on the current
    55  // context. For error status codes 400 and 401, the error will be serialized as
    56  // JSON or XML (via `ctx.DefaultProvider`) and returned in the HTTP response.
    57  // For error status code 500, no identifying information from the error will be
    58  // returned in the HTTP response.
    59  //
    60  // A 401 Unauthorized will be returned in the following cases:
    61  // - The XFCC header is missing
    62  // - The XFCC header (after parsing) contains zero elements or multiple elements
    63  //   (this code expects exactly one XFCC element, under the assumption that the
    64  //   Envoy `ForwardClientCertDetails` setting is configured to `SANITIZE_SET`)
    65  // - The values from the XFCC header fail custom validation provided by `cip` or
    66  //   `verifiers`. For example, if the client identity is contained in a deny
    67  //   list, this would be considered a validation error.
    68  //
    69  // A 400 Bad Request will be returned in the following cases:
    70  // - The XFCC header cannot be parsed
    71  // - Custom parsing / extraction done by `cip` fails. For example, in cases
    72  //   where the `URI` field in the XFCC is expected to be a valid SPIFFE URI
    73  //   with a valid Kubernetes workload identifier, if the `URI` field does
    74  //   not follow that format (e.g. `urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66`)
    75  //   this would be considered an extraction error.
    76  //
    77  // A 500 Internal Server Error will be returned if the error is unrelated to
    78  // validating the XFCC header or to parsing / extracting values from the XFCC
    79  // header.
    80  func ClientIdentityRequired(cip IdentityProvider, verifiers ...VerifyXFCC) web.Middleware {
    81  	return func(action web.Action) web.Action {
    82  		return func(ctx *web.Ctx) web.Result {
    83  			clientIdentity, err := ExtractAndVerifyClientIdentity(ctx.Request, cip, verifiers...)
    84  			if IsValidationError(err) {
    85  				return ctx.DefaultProvider.Status(http.StatusUnauthorized, err)
    86  			}
    87  			if IsExtractionError(err) {
    88  				// NOTE: We don't use `ctx.DefaultProvider.BadRequest()` because
    89  				//       we want to allow serializing `err` as JSON if possible.
    90  				//       The JSON provider just uses `err.Error()` for the response.
    91  				return ctx.DefaultProvider.Status(http.StatusBadRequest, err)
    92  			}
    93  			if err != nil {
    94  				return ctx.DefaultProvider.InternalError(nil)
    95  			}
    96  
    97  			ctx.WithContext(WithClientIdentity(ctx.Context(), clientIdentity))
    98  			return action(ctx)
    99  		}
   100  	}
   101  }
   102  
   103  // ClientIdentityAware produces a middleware function nearly identical to
   104  // `ClientIdentityRequired`. The primary difference is that this middleware will
   105  // **not** return an error HTTP response for extraction or validation errors;
   106  // it will still return a 500 Internal Server Error in unexpected failures.
   107  // In cases of extraction or validation errors, the middleware will pass along
   108  // to the next `action` and the client identity is not set on the current context.
   109  func ClientIdentityAware(cip IdentityProvider, verifiers ...VerifyXFCC) web.Middleware {
   110  	return func(action web.Action) web.Action {
   111  		return func(ctx *web.Ctx) web.Result {
   112  			clientIdentity, err := ExtractAndVerifyClientIdentity(ctx.Request, cip, verifiers...)
   113  			// Early exit for a no-op in cases of validation or extraction error.
   114  			if IsValidationError(err) || IsExtractionError(err) {
   115  				return action(ctx)
   116  			}
   117  
   118  			if err != nil {
   119  				return ctx.DefaultProvider.InternalError(nil)
   120  			}
   121  
   122  			currentCtx := ctx.Context()
   123  			ctx.WithContext(WithClientIdentity(currentCtx, clientIdentity))
   124  			return action(ctx)
   125  		}
   126  	}
   127  }