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  }