github.com/blend/go-sdk@v1.20220411.3/envoyutil/middleware_test.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_test
     9  
    10  import (
    11  	"context"
    12  	"encoding/json"
    13  	"net/http"
    14  	"testing"
    15  
    16  	sdkAssert "github.com/blend/go-sdk/assert"
    17  	"github.com/blend/go-sdk/r2"
    18  	"github.com/blend/go-sdk/web"
    19  
    20  	"github.com/blend/go-sdk/envoyutil"
    21  )
    22  
    23  func TestWithClientIdentity(t *testing.T) {
    24  	assert := sdkAssert.New(t)
    25  
    26  	ctx := context.Background()
    27  	newCtx := envoyutil.WithClientIdentity(ctx, "web.site")
    28  	assert.Empty(envoyutil.GetClientIdentity(ctx))
    29  	assert.Equal("web.site", envoyutil.GetClientIdentity(newCtx))
    30  }
    31  
    32  func TestClientIdentityRequired(t *testing.T) {
    33  	assert := sdkAssert.New(t)
    34  
    35  	app := web.MustNew()
    36  	var capturedContext *web.Ctx
    37  	cip := envoyutil.SPIFFEClientIdentityProvider(
    38  		envoyutil.OptDeniedIdentities("gw.blend"),
    39  	)
    40  	verifier := envoyutil.SPIFFEServerIdentityProvider(
    41  		envoyutil.OptAllowedIdentities("idea.blend"),
    42  	)
    43  	app.GET(
    44  		"/",
    45  		func(ctx *web.Ctx) web.Result {
    46  			capturedContext = ctx
    47  			return web.JSON.OK()
    48  		},
    49  		envoyutil.ClientIdentityRequired(cip, verifier),
    50  		web.JSONProviderAsDefault,
    51  	)
    52  
    53  	body, meta, err := web.MockGet(app, "/").Bytes()
    54  	assert.Nil(err)
    55  	assert.Equal(http.StatusUnauthorized, meta.StatusCode, "Fail on missing header")
    56  	assert.Nil(capturedContext)
    57  	var expected error = &envoyutil.XFCCValidationError{Class: envoyutil.ErrMissingXFCC}
    58  	invalidXFCCJSONEqual(assert, expected, body)
    59  
    60  	xfcc := `""`
    61  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
    62  	assert.Nil(err)
    63  	assert.Equal(http.StatusBadRequest, meta.StatusCode, "Fail on empty header")
    64  	assert.Nil(capturedContext)
    65  	expected = &envoyutil.XFCCExtractionError{Class: envoyutil.ErrInvalidXFCC, XFCC: xfcc}
    66  	invalidXFCCJSONEqual(assert, expected, body)
    67  
    68  	xfcc = "something=bad"
    69  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
    70  	assert.Nil(err)
    71  	assert.Equal(http.StatusBadRequest, meta.StatusCode, "Fail on malformed header")
    72  	assert.Nil(capturedContext)
    73  	expected = &envoyutil.XFCCExtractionError{Class: envoyutil.ErrInvalidXFCC, XFCC: xfcc}
    74  	invalidXFCCJSONEqual(assert, expected, body)
    75  
    76  	xfcc = "By=spiffe://cluster.local/ns/blend/sa/idea;URI=spiffe://cluster.local/ns/blend/sa/should-end/sa/extra"
    77  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
    78  	assert.Nil(err)
    79  	assert.Equal(http.StatusBadRequest, meta.StatusCode, "Fail on unexpected SPIFFE format in `URI`")
    80  	assert.Nil(capturedContext)
    81  	expected = &envoyutil.XFCCExtractionError{Class: envoyutil.ErrInvalidClientIdentity, XFCC: xfcc}
    82  	invalidXFCCJSONEqual(assert, expected, body)
    83  
    84  	xfcc = `By=spiffe://cluster.local/ns/blend/sa/idea;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"`
    85  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
    86  	assert.Nil(err)
    87  	assert.Equal(http.StatusUnauthorized, meta.StatusCode, "Fail on missing client identity")
    88  	assert.Nil(capturedContext)
    89  	expected = &envoyutil.XFCCValidationError{Class: envoyutil.ErrInvalidClientIdentity, XFCC: xfcc}
    90  	invalidXFCCJSONEqual(assert, expected, body)
    91  
    92  	xfcc = `By=spiffe://cluster.local/ns/blend/sa/idea;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=spiffe://cluster.local/ns/blend/sa/gw`
    93  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
    94  	assert.Nil(err)
    95  	assert.Equal(http.StatusUnauthorized, meta.StatusCode, "Fail on denied client identity")
    96  	assert.Nil(capturedContext)
    97  	expected = &envoyutil.XFCCValidationError{
    98  		Class: envoyutil.ErrDeniedClientIdentity,
    99  		XFCC:  xfcc,
   100  		// NOTE: This should really be a `map[string]string`. We use a `map[string]interface{}`
   101  		//       so that the comparison in `invalidXFCCJSONEqual()` passes.
   102  		Metadata: map[string]interface{}{"clientIdentity": "gw.blend"},
   103  	}
   104  	invalidXFCCJSONEqual(assert, expected, body)
   105  
   106  	xfcc = `By=spiffe://cluster.local/ns/blend/sa/idea;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=spiffe://cluster.local/ns/blend/sa/twtr`
   107  	meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Discard()
   108  	assert.Nil(err)
   109  	assert.Equal(http.StatusOK, meta.StatusCode, "Success on valid header")
   110  	assert.NotNil(capturedContext)
   111  	assert.Equal("twtr.blend", envoyutil.GetClientIdentity(capturedContext.Context()))
   112  	capturedContext = nil
   113  
   114  	xfcc = `By=mailto:John.Doe@example.com;URI=spiffe://cluster.local/ns/blend/sa/peas`
   115  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
   116  	assert.Nil(err)
   117  	assert.Equal(http.StatusBadRequest, meta.StatusCode, "Fail on invalid server identity")
   118  	assert.Nil(capturedContext)
   119  	expected = &envoyutil.XFCCExtractionError{
   120  		Class: envoyutil.ErrInvalidServerIdentity,
   121  		XFCC:  xfcc,
   122  	}
   123  	invalidXFCCJSONEqual(assert, expected, body)
   124  
   125  	xfcc = `By=spiffe://cluster.local/ns/blend/sa/outside;URI=spiffe://cluster.local/ns/blend/sa/peas`
   126  	body, meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, xfcc)).Bytes()
   127  	assert.Nil(err)
   128  	assert.Equal(http.StatusUnauthorized, meta.StatusCode, "Fail on wrong server identity")
   129  	assert.Nil(capturedContext)
   130  	expected = &envoyutil.XFCCValidationError{
   131  		Class: envoyutil.ErrDeniedServerIdentity,
   132  		XFCC:  xfcc,
   133  		// NOTE: This should really be a `map[string]string`. We use a `map[string]interface{}`
   134  		//       so that the comparison in `invalidXFCCJSONEqual()` passes.
   135  		Metadata: map[string]interface{}{"serverIdentity": "outside.blend"},
   136  	}
   137  	invalidXFCCJSONEqual(assert, expected, body)
   138  
   139  	// Unrecoverable error: here we simulate `envoyutil` user error by using
   140  	// `nil` for `cip`.
   141  	app = web.MustNew()
   142  	app.GET(
   143  		"/",
   144  		func(ctx *web.Ctx) web.Result {
   145  			return web.JSON.OK()
   146  		},
   147  		envoyutil.ClientIdentityRequired(nil),
   148  		web.JSONProviderAsDefault,
   149  	)
   150  	body, meta, err = web.MockGet(app, "/").Bytes()
   151  	assert.Nil(err)
   152  	assert.Equal(http.StatusInternalServerError, meta.StatusCode, "Fail on unrecoverable")
   153  	assert.Equal("\"Internal Server Error\"\n", string(body))
   154  }
   155  
   156  func TestClientIdentityAware(t *testing.T) {
   157  	assert := sdkAssert.New(t)
   158  
   159  	app := web.MustNew()
   160  	cip := envoyutil.SPIFFEClientIdentityProvider(
   161  		envoyutil.OptDeniedIdentities("gw.blend"),
   162  	)
   163  	verifier := envoyutil.SPIFFEServerIdentityProvider(
   164  		envoyutil.OptAllowedIdentities("quasar.blend"),
   165  	)
   166  	app.GET("/",
   167  		func(_ *web.Ctx) web.Result {
   168  			return web.JSON.OK()
   169  		},
   170  		envoyutil.ClientIdentityAware(cip, verifier),
   171  		web.JSONProviderAsDefault,
   172  	)
   173  
   174  	meta, err := web.MockGet(app, "/").Discard()
   175  	assert.Nil(err)
   176  	assert.Equal(http.StatusOK, meta.StatusCode, "Don't fail on missing header")
   177  
   178  	meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, "something=bad")).Discard()
   179  	assert.Nil(err)
   180  	assert.Equal(http.StatusOK, meta.StatusCode, "Don't fail on malformed header")
   181  
   182  	meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, `By=spiffe://cluster.local/ns/blend/sa/quasar;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"`)).Discard()
   183  	assert.Nil(err)
   184  	assert.Equal(http.StatusOK, meta.StatusCode, "Don't fail on missing workload")
   185  
   186  	meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, `By=spiffe://cluster.local/ns/blend/sa/quasar;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=spiffe://cluster.local/ns/blend/sa/gw`)).Discard()
   187  	assert.Nil(err)
   188  	assert.Equal(http.StatusOK, meta.StatusCode, "Don't fail on denied client identity")
   189  
   190  	meta, err = web.MockGet(app, "/", r2.OptHeaderValue(envoyutil.HeaderXFCC, `By=spiffe://cluster.local/ns/blend/sa/quasar;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=spiffe://cluster.local/ns/blend/sa/books`)).Discard()
   191  	assert.Nil(err)
   192  	assert.Equal(http.StatusOK, meta.StatusCode, "Success on valid header")
   193  
   194  	// Unrecoverable error: here we simulate `envoyutil` user error by using
   195  	// `nil` for `cip`.
   196  	app = web.MustNew()
   197  	app.GET(
   198  		"/",
   199  		func(ctx *web.Ctx) web.Result {
   200  			return web.JSON.OK()
   201  		},
   202  		envoyutil.ClientIdentityAware(nil),
   203  		web.JSONProviderAsDefault,
   204  	)
   205  	body, meta, err := web.MockGet(app, "/").Bytes()
   206  	assert.Nil(err)
   207  	assert.Equal(http.StatusInternalServerError, meta.StatusCode, "Fail on unrecoverable")
   208  	assert.Equal("\"Internal Server Error\"\n", string(body))
   209  }
   210  
   211  func invalidXFCCJSONEqual(assert *sdkAssert.Assertions, expected error, actual []byte) {
   212  	switch expected.(type) {
   213  	case *envoyutil.XFCCExtractionError:
   214  		unmarshaledActual := &envoyutil.XFCCExtractionError{}
   215  		err := json.Unmarshal(actual, unmarshaledActual)
   216  		assert.Nil(err)
   217  		assert.Equal(expected, unmarshaledActual)
   218  	case *envoyutil.XFCCValidationError:
   219  		unmarshaledActual := &envoyutil.XFCCValidationError{}
   220  		err := json.Unmarshal(actual, unmarshaledActual)
   221  		assert.Nil(err)
   222  		assert.Equal(expected, unmarshaledActual)
   223  	default:
   224  		assert.FailNow()
   225  	}
   226  }