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 }