istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/server/ca/authenticate/kubeauth/kube_jwt_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 kubeauth 16 17 import ( 18 "context" 19 "reflect" 20 "testing" 21 22 "google.golang.org/grpc/metadata" 23 k8sauth "k8s.io/api/authentication/v1" 24 "k8s.io/apimachinery/pkg/runtime" 25 "k8s.io/client-go/kubernetes" 26 "k8s.io/client-go/kubernetes/fake" 27 ktesting "k8s.io/client-go/testing" 28 29 meshconfig "istio.io/api/mesh/v1alpha1" 30 "istio.io/istio/pkg/cluster" 31 "istio.io/istio/pkg/config/constants" 32 "istio.io/istio/pkg/security" 33 "istio.io/istio/pkg/spiffe" 34 "istio.io/istio/pkg/test/util/assert" 35 ) 36 37 type mockMeshConfigHolder struct { 38 trustDomain string 39 } 40 41 func (mh mockMeshConfigHolder) Mesh() *meshconfig.MeshConfig { 42 return &meshconfig.MeshConfig{ 43 TrustDomain: mh.trustDomain, 44 } 45 } 46 47 func TestNewKubeJWTAuthenticator(t *testing.T) { 48 meshHolder := mockMeshConfigHolder{"testdomain.com"} 49 authenticator := NewKubeJWTAuthenticator(meshHolder, nil, constants.DefaultClusterName, nil) 50 expectedAuthenticator := &KubeJWTAuthenticator{ 51 meshHolder: meshHolder, 52 clusterID: constants.DefaultClusterName, 53 } 54 if !reflect.DeepEqual(authenticator, expectedAuthenticator) { 55 t.Errorf("Unexpected authentication result: want %v but got %v", 56 expectedAuthenticator, authenticator) 57 } 58 } 59 60 func TestAuthenticate(t *testing.T) { 61 primaryCluster := constants.DefaultClusterName 62 remoteCluster := cluster.ID("remote") 63 invlidToken := "invalid-token" 64 meshHolder := mockMeshConfigHolder{"example.com"} 65 spiffe.SetTrustDomain("example.com") 66 67 testCases := map[string]struct { 68 remoteCluster bool 69 metadata metadata.MD 70 token string 71 expectedID string 72 expectedErrMsg string 73 }{ 74 "No bearer token": { 75 metadata: metadata.MD{ 76 "clusterid": []string{primaryCluster}, 77 "authorization": []string{ 78 "Basic callername", 79 }, 80 }, 81 expectedErrMsg: "target JWT extraction error: no bearer token exists in HTTP authorization header", 82 }, 83 "token not authenticated": { 84 token: invlidToken, 85 metadata: metadata.MD{ 86 "clusterid": []string{primaryCluster}, 87 "authorization": []string{ 88 "Basic callername", 89 }, 90 }, 91 expectedErrMsg: `failed to validate the JWT from cluster "Kubernetes": the token is not authenticated`, 92 }, 93 "token authenticated": { 94 token: "bearer-token", 95 metadata: metadata.MD{ 96 "clusterid": []string{primaryCluster}, 97 "authorization": []string{ 98 "Basic callername", 99 }, 100 }, 101 expectedID: spiffe.MustGenSpiffeURI("default", "example-pod-sa"), 102 expectedErrMsg: "", 103 }, 104 "not found remote cluster results in error": { 105 remoteCluster: false, 106 token: "bearer-token", 107 metadata: metadata.MD{ 108 "clusterid": []string{"non-exist"}, 109 "authorization": []string{ 110 "Basic callername", 111 }, 112 }, 113 expectedErrMsg: "could not get cluster non-exist's kube client", 114 }, 115 } 116 117 for id, tc := range testCases { 118 t.Run(id, func(t *testing.T) { 119 ctx := context.Background() 120 if tc.metadata != nil { 121 if tc.token != "" { 122 token := security.BearerTokenPrefix + tc.token 123 tc.metadata.Append("authorization", token) 124 } 125 ctx = metadata.NewIncomingContext(ctx, tc.metadata) 126 } 127 128 tokenReview := &k8sauth.TokenReview{ 129 Spec: k8sauth.TokenReviewSpec{ 130 Token: tc.token, 131 }, 132 } 133 134 tokenReview.Status.Audiences = []string{} 135 if tc.token != invlidToken { 136 tokenReview.Status.Authenticated = true 137 } 138 tokenReview.Status.User = k8sauth.UserInfo{ 139 Username: "system:serviceaccount:default:example-pod-sa", 140 Groups: []string{"system:serviceaccounts"}, 141 } 142 143 client := fake.NewSimpleClientset() 144 if !tc.remoteCluster { 145 client.PrependReactor("create", "tokenreviews", func(action ktesting.Action) (bool, runtime.Object, error) { 146 return true, tokenReview, nil 147 }) 148 } 149 150 remoteKubeClientGetter := func(clusterID cluster.ID) kubernetes.Interface { 151 if clusterID == remoteCluster { 152 client := fake.NewSimpleClientset() 153 if tc.remoteCluster { 154 client.PrependReactor("create", "tokenreviews", func(action ktesting.Action) (bool, runtime.Object, error) { 155 return true, tokenReview, nil 156 }) 157 } 158 } 159 return nil 160 } 161 162 authenticator := NewKubeJWTAuthenticator(meshHolder, client, constants.DefaultClusterName, remoteKubeClientGetter) 163 actualCaller, err := authenticator.Authenticate(security.AuthContext{GrpcContext: ctx}) 164 if len(tc.expectedErrMsg) > 0 { 165 if err == nil { 166 t.Errorf("Case %s: Succeeded. Error expected: %v", id, err) 167 } else if err.Error() != tc.expectedErrMsg { 168 t.Errorf("Case %s: Incorrect error message: \n%s\nVS\n%s", 169 id, err.Error(), tc.expectedErrMsg) 170 } 171 return 172 } else if err != nil { 173 t.Errorf("Case %s: Unexpected Error: %v", id, err) 174 return 175 } 176 177 expectedCaller := &security.Caller{ 178 AuthSource: security.AuthSourceIDToken, 179 Identities: []string{tc.expectedID}, 180 KubernetesInfo: security.KubernetesInfo{ 181 PodNamespace: "default", 182 PodServiceAccount: "example-pod-sa", 183 }, 184 } 185 186 assert.Equal(t, actualCaller, expectedCaller) 187 }) 188 } 189 } 190 191 func TestIsAllowedKubernetesAudience(t *testing.T) { 192 tests := []struct { 193 in string 194 want bool 195 }{ 196 {"kubernetes.default.svc", true}, 197 {"kubernetes.default.svc.cluster.local", true}, 198 {"https://kubernetes.default.svc", true}, 199 {"https://kubernetes.default.svc.cluster.local", true}, 200 {"foo.default.svc", false}, 201 {"foo.default.svc:80", false}, 202 {"https://foo.default.svc:80", false}, 203 } 204 for _, tt := range tests { 205 t.Run(tt.in, func(t *testing.T) { 206 if got := isAllowedKubernetesAudience(tt.in); got != tt.want { 207 t.Errorf("isAllowedKubernetesAudience() = %v, want %v", got, tt.want) 208 } 209 }) 210 } 211 }