istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/credentialfetcher/plugin/gce_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package plugin 16 17 import ( 18 "fmt" 19 "os" 20 "testing" 21 "time" 22 23 "github.com/google/uuid" 24 25 "istio.io/istio/pkg/test/util/retry" 26 ) 27 28 var ( 29 // thirdPartyJwt is generated in a testing K8s cluster, using the "istio-token" projected volume. 30 // Token is issued at 2020-04-04 22:13:54 Pacific Daylight time. 31 // Expiration time is 2020-04-05 10:13:54 Pacific Daylight time. 32 thirdPartyJwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9." + 33 "eyJhdWQiOlsieW9uZ2dhbmdsLWlzdGlvLTQuc3ZjLmlkLmdvb2ciXSwiZXhwIjoxNTg2MTA2ODM0LCJpYXQiOjE1" + 34 "ODYwNjM2MzQsImlzcyI6Imh0dHBzOi8vY29udGFpbmVyLmdvb2dsZWFwaXMuY29tL3YxL3Byb2plY3RzL3lvbmdn" + 35 "YW5nbC1pc3Rpby00L2xvY2F0aW9ucy91cy1jZW50cmFsMS1hL2NsdXN0ZXJzL2NsdXN0ZXItMyIsImt1YmVybmV0" + 36 "ZXMuaW8iOnsibmFtZXNwYWNlIjoiZm9vIiwicG9kIjp7Im5hbWUiOiJodHRwYmluLTY0Nzc2YmY3OGQtanFsNWIi" + 37 "LCJ1aWQiOiI5YWQ3NTcxYi03NjBhLTExZWEtODllNy00MjAxMGE4MDAxYzEifSwic2VydmljZWFjY291bnQiOnsi" + 38 "bmFtZSI6Imh0dHBiaW4iLCJ1aWQiOiI5OWY2NWY1MC03NjBhLTExZWEtODllNy00MjAxMGE4MDAxYzEifX0sIm5i" + 39 "ZiI6MTU4NjA2MzYzNCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmZvbzpodHRwYmluIn0.XWSCdarBR0cx" + 40 "MlVV5X9pvgI9m0lyO-17B45aBKJBIQjvjluKXqxnCuIeD3X2ItLkCUzmKUa3ftTjUUov1MJ89MdBngNfUP7IfwnD" + 41 "2dBl7Jtju0-Ks7aTFOkgtoMYqNnQ1VSDTAOfNpdZVUnsR_oY8obXSQR_H4uMcaNOGED2RX5HLBWFlvymtn4JXuyg" + 42 "_rpOrJ8dv-snrmO3LT9y-zaUnZqSceDC8skzStrJIRvsRkO8GEcoQd5VwDn-UVgOcqWb-S-vgSjdtwBsnGPXsh_I" + 43 "NZCq3ftr0Qu8-IxsIjpMjhLmAGTH1bR324aqLTYhAXp6fk06Pe3T9stCY5acSeadKA" 44 // firstPartyJwt is generated in a testing K8s cluster. It is the default service account JWT. 45 // No expiration time. 46 firstPartyJwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9." + 47 "eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9u" + 48 "YW1lc3BhY2UiOiJmb28iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiaHR0cGJp" + 49 "bi10b2tlbi14cWRncCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUi" + 50 "OiJodHRwYmluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiOTlm" + 51 "NjVmNTAtNzYwYS0xMWVhLTg5ZTctNDIwMTBhODAwMWMxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmZv" + 52 "bzpodHRwYmluIn0.4kIl9TRjXEw6DfhtR-LdpxsAlYjJgC6Ly1DY_rqYY4h0haxcXB3kYZ3b2He-3fqOBryz524W" + 53 "KkZscZgvs5L-sApmvlqdUG61TMAl7josB0x4IMHm1NS995LNEaXiI4driffwfopvqc_z3lVKfbF9j-mBgnCepxz3" + 54 "UyWo5irFa3qcwbOUB9kuuUNGBdtbFBN5yIYLpfa9E-MtTX_zJ9fQ9j2pi8Z4ljii0tEmPmRxokHkmG_xNJjUkxKU" + 55 "WZf4bLDdCEjVFyshNae-FdxiUVyeyYorTYzwZZYQch9MJeedg4keKKUOvCCJUlKixd2qAe-H7r15RPmo4AU5O5YL" + 56 "65xiNg" 57 ) 58 59 func TestShouldRotate(t *testing.T) { 60 jwtExp := time.Date(2020, time.April, 5, 10, 13, 54, 0, time.FixedZone("PDT", -int((7*time.Hour).Seconds()))) 61 testCases := map[string]struct { 62 jwt string 63 now time.Time 64 expectedRotate bool 65 }{ 66 "remaining life time is in grace period": { 67 jwt: thirdPartyJwt, 68 now: jwtExp.Add(time.Duration(-10) * time.Minute), 69 expectedRotate: true, 70 }, 71 "remaining life time is not in grace period": { 72 jwt: thirdPartyJwt, 73 now: jwtExp.Add(time.Duration(-30) * time.Minute), 74 expectedRotate: false, 75 }, 76 "no cached credential": { 77 now: time.Now(), 78 expectedRotate: true, 79 }, 80 "no expiration in token": { 81 jwt: firstPartyJwt, 82 now: time.Now(), 83 expectedRotate: true, 84 }, 85 "invalid token": { 86 jwt: "invalid-token", 87 now: time.Now(), 88 expectedRotate: true, 89 }, 90 } 91 92 for id, tc := range testCases { 93 t.Run(id, func(t *testing.T) { 94 p := GCEPlugin{ 95 tokenCache: tc.jwt, 96 } 97 if rotate := p.shouldRotate(tc.now); rotate != tc.expectedRotate { 98 t.Errorf("%s, shouldRotate(%s)=%t, expected %t", 99 id, tc.now.String(), rotate, tc.expectedRotate) 100 } 101 }) 102 } 103 } 104 105 func creatJWTFile(path string) error { 106 if path == "" { 107 return nil 108 } 109 f, err := os.Create(path) 110 if err != nil { 111 return err 112 } 113 f.Close() 114 return nil 115 } 116 117 func getJWTFromFile(path string) (string, error) { 118 if path == "" { 119 return "", nil 120 } 121 data, err := os.ReadFile(path) 122 if err != nil { 123 return "", err 124 } 125 return string(data), nil 126 } 127 128 func getTokenFromServer(t *testing.T, p *GCEPlugin, callReps int) ([]string, []error) { 129 t.Helper() 130 var tokens []string 131 var errs []error 132 for i := 0; i < callReps; i++ { 133 token, err := p.GetPlatformCredential() 134 tokens = append(tokens, token) 135 errs = append(errs, err) 136 } 137 return tokens, errs 138 } 139 140 func verifyError(t *testing.T, id string, errs []error, expectedErr error) { 141 t.Helper() 142 for _, err := range errs { 143 if err == nil && expectedErr != nil || err != nil && expectedErr == nil { 144 t.Errorf("%s, GetPlatformCredential() returns err: %v, want: %v", id, err, expectedErr) 145 } else if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() { 146 t.Errorf("%s, GetPlatformCredential() returns err: %v, want: %v", id, err, expectedErr) 147 } 148 } 149 } 150 151 func verifyToken(t *testing.T, id, jwtPath string, tokens []string, expectedToken string) { 152 t.Helper() 153 for i, token := range tokens { 154 // if expectedToken is not set, mock metadata server returns an auto-generated fake token. 155 wantToken := fmt.Sprintf("%s%d", fakeTokenPrefix, i+1) 156 if expectedToken != "" { 157 wantToken = expectedToken 158 } 159 if token != wantToken { 160 t.Errorf("%s, #%d call to GetPlatformCredential() returns token: %s, want: %s", 161 id, i, token, wantToken) 162 } 163 if jwtPath != "" && i+1 == len(tokens) { 164 // If this is the last round, and JWT path is set, check JWT in the file. 165 jwtFile, err := getJWTFromFile(jwtPath) 166 if err != nil { 167 t.Fatalf("%s, failed to read token from file %s: %v", id, jwtPath, err) 168 } 169 if jwtFile != wantToken { 170 t.Errorf("%s, %s has token %s, want %s", id, jwtPath, jwtFile, wantToken) 171 } 172 } 173 } 174 } 175 176 func TestGCEPlugin(t *testing.T) { 177 testCases := map[string]struct { 178 jwt string 179 jwtPath string 180 expectedToken string 181 expectedCall int 182 expectedErr error 183 }{ 184 "get VM credential": { 185 jwt: thirdPartyJwt, 186 jwtPath: fmt.Sprintf("/tmp/security-pkg-credentialfetcher-plugin-gcetest-%s", uuid.New().String()), 187 expectedToken: thirdPartyJwt, 188 expectedCall: 1, 189 }, 190 "jwt path not set": { 191 jwt: thirdPartyJwt, 192 expectedCall: 1, 193 expectedErr: fmt.Errorf("jwtPath is unset"), 194 }, 195 "fetch credential multiple times": { 196 expectedCall: 5, 197 jwtPath: fmt.Sprintf("/tmp/security-pkg-credentialfetcher-plugin-gcetest-%s", uuid.New().String()), 198 }, 199 } 200 201 SetTokenRotation(false) 202 ms, err := StartMetadataServer() 203 if err != nil { 204 t.Fatalf("StartMetadataServer() returns err: %v", err) 205 } 206 t.Cleanup(func() { 207 ms.Stop() 208 SetTokenRotation(true) 209 }) 210 211 for id, tc := range testCases { 212 p := GCEPlugin{ 213 tokenCache: tc.jwt, 214 jwtPath: tc.jwtPath, 215 } 216 if err := creatJWTFile(tc.jwtPath); err != nil { 217 t.Fatalf("%s, creatJWTFile() returns err: %v", id, err) 218 } 219 ms.Reset() 220 ms.setToken(tc.jwt) 221 222 tokens, errs := getTokenFromServer(t, &p, tc.expectedCall) 223 224 verifyError(t, id, errs, tc.expectedErr) 225 if tc.expectedErr == nil { 226 if ms.NumGetTokenCall() != tc.expectedCall { 227 t.Errorf("%s, metadata server receives %d calls, want %d", 228 id, ms.NumGetTokenCall(), tc.expectedCall) 229 } 230 if tc.jwtPath != "" { 231 verifyToken(t, id, tc.jwtPath, tokens, tc.expectedToken) 232 } 233 } 234 } 235 } 236 237 func TestTokenRotationJob(t *testing.T) { 238 testCases := map[string]struct { 239 jwt string 240 jwtPath string 241 expectedToken string 242 expectedCall int 243 }{ 244 // mock metadata server returns an expired token, that forces rotation job 245 // to fetch new token during each rotation. 246 "expired token needs rotation": { 247 jwt: thirdPartyJwt, 248 jwtPath: fmt.Sprintf("/tmp/security-pkg-credentialfetcher-plugin-gcetest-%s", uuid.New().String()), 249 expectedToken: thirdPartyJwt, 250 expectedCall: 3, 251 }, 252 // mock metadata server returns a token which has no exp field, that forces rotation job 253 // to fetch new token during each rotation. 254 "token with no expiration time needs rotation": { 255 jwt: firstPartyJwt, 256 jwtPath: fmt.Sprintf("/tmp/security-pkg-credentialfetcher-plugin-gcetest-%s", uuid.New().String()), 257 expectedToken: firstPartyJwt, 258 expectedCall: 3, 259 }, 260 // mock metadata server returns an invalid token, that forces rotation job 261 // to fetch new token during each rotation. 262 "invalid token needs rotation": { 263 jwt: "invalid-token-section-1.invalid-token-section-2", 264 jwtPath: fmt.Sprintf("/tmp/security-pkg-credentialfetcher-plugin-gcetest-%s", uuid.New().String()), 265 expectedCall: 3, 266 expectedToken: "invalid-token-section-1.invalid-token-section-2", 267 }, 268 } 269 270 // starts rotation job every 0.5 second. 271 rotationInterval = 500 * time.Millisecond 272 SetTokenRotation(true) 273 ms, err := StartMetadataServer() 274 if err != nil { 275 t.Fatalf("StartMetadataServer() returns err: %v", err) 276 } 277 t.Cleanup(func() { 278 ms.Stop() 279 rotationInterval = 5 * time.Minute 280 }) 281 282 for id, tc := range testCases { 283 // These tests should not run in parallel because they share one metadata server. 284 t.Run(id, func(t *testing.T) { 285 p := CreateGCEPlugin("", tc.jwtPath, "") 286 if err := creatJWTFile(tc.jwtPath); err != nil { 287 t.Fatalf("%s, creatJWTFile() returns err: %v", id, err) 288 } 289 ms.Reset() 290 ms.setToken(tc.jwt) 291 292 // Verify that rotation job is kicked multiple times. 293 retryTimeout := time.Duration(5+tc.expectedCall) * rotationInterval 294 retry.UntilSuccessOrFail(t, func() error { 295 callNumber := ms.NumGetTokenCall() 296 if callNumber < tc.expectedCall { 297 return fmt.Errorf("%s, got %d token fetch calls, expected %d", 298 id, callNumber, tc.expectedCall) 299 } 300 return nil 301 }, retry.Delay(time.Second), retry.Timeout(retryTimeout)) 302 303 p.Stop() 304 }) 305 } 306 }