diff --git a/internal/concierge/impersonator/impersonator.go b/internal/concierge/impersonator/impersonator.go index 9c06d3f6d..b145856d0 100644 --- a/internal/concierge/impersonator/impersonator.go +++ b/internal/concierge/impersonator/impersonator.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package impersonator @@ -236,12 +236,12 @@ func newInternal( serverConfig.AuditPolicyRuleEvaluator = policy.NewFakePolicyRuleEvaluator(auditinternal.LevelMetadata, nil) serverConfig.AuditBackend = &auditfake.Backend{} - // Probe the API server to figure out if anonymous auth is enabled. - anonymousAuthEnabled, err := isAnonymousAuthEnabled(kubeClientUnsafeForProxying.JSONConfig) + // Probe the Kubernetes API server to figure out if anonymous auth is enabled. + anonymousAuthProbeResult, err := isAnonymousAuthEnabled(kubeClientUnsafeForProxying.JSONConfig) if err != nil { return nil, fmt.Errorf("could not detect if anonymous authentication is enabled: %w", err) } - plog.Debug("anonymous authentication probed", "anonymousAuthEnabled", anonymousAuthEnabled) + plog.Debug("anonymous authentication probed", "results", anonymousAuthProbeResult) // if we ever start unioning a TCR bearer token authenticator with serverConfig.Authenticator // then we will need to update the related assumption in tokenPassthroughRoundTripper @@ -251,8 +251,15 @@ func newInternal( RequestFunc: func(req *http.Request) (*authenticator.Response, bool, error) { resp, ok, err := delegatingAuthenticator.AuthenticateRequest(req) - // anonymous auth is enabled so no further check is necessary - if anonymousAuthEnabled { + reqIsHealthCheck := isRequestForHealthCheck(req) + + if anonymousAuthProbeResult.HealthCheckEndpointsAllowAnonAuth && reqIsHealthCheck { + // anonymous auth is enabled for health check endpoints, so no further check is necessary + return resp, ok, err + } + + if anonymousAuthProbeResult.OtherEndpointsAllowAnonAuth && !reqIsHealthCheck { + // anonymous auth is enabled for all other endpoints, so no further check is necessary return resp, ok, err } @@ -266,6 +273,10 @@ func newInternal( return resp, ok, err } + // If we got this far, then anonymous auth is disabled for this request's path, + // and authentication succeeded, and the user is system:anonymous. + // Now we want to allow these anonymous users to make requests _only_ to the TCR endpoint. + reqInfo, ok := genericapirequest.RequestInfoFrom(req.Context()) if !ok { return nil, false, constable.Error("no RequestInfo found in the context") @@ -276,7 +287,7 @@ func newInternal( return nil, false, nil } - // any resource besides TKR should not be authenticated + // any resource besides TKR should not be authenticated because anonymous auth is disabled on this cluster if !isTokenCredReq(reqInfo) { return nil, false, nil } @@ -370,7 +381,46 @@ func getReverseProxyClient(baseConfig *rest.Config, cache tokenclient.ExpiringSi return kubeclient.New(kubeclient.WithConfig(impersonationProxyRestConfig)) } -func isAnonymousAuthEnabled(config *rest.Config) (bool, error) { +func isRequestForHealthCheck(req *http.Request) bool { + if req == nil || req.URL == nil { + // Shouldn't really happen but easy enough to handle here. + return false + } + + path := req.URL.Path + + // See https://kubernetes.io/docs/reference/using-api/health-checks. + // Although there are sub-paths for these endpoints, e.g. /healthz/etcd, the sub-paths are not made available + // for anonymous auth by GKE when anonymous auth is otherwise disabled, so let's mirror that behavior here. + switch path { + case "/healthz", "/readyz", "/livez": + return true + default: + return false + } +} + +// anonAuthEnablement allows us to characterize the most common configurations for anonymous authentication: +// +// 1. Anonymous auth is disabled for the whole k8s API (e.g. AKS clusters). +// 2. Anonymous auth is enabled for the whole k8s API (e.g. GKE clusters before Kubernetes 1.35). +// 3. In newer clusters, anonymous auth can be selectively enabled only for certain API paths. +// See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-authenticator-configuration. +// Usually this is used to enable anonymous auth only for the three health check endpoints, and disable it for +// all other endpoints. This configuration was adpoted by GKE clusters starting with Kube 1.35 (as the default, but +// can be overridden by the user during cluster creation to instead allow anon auth for the whole API) +// and by EKS clusters starting with Kube 1.32. +// +// Of course, other configurations of which API paths allow should anonymous auth are possible, but we have no practical +// way of auto-detecting them here. We could alternatively add new Pinniped configuration options instead of trying to +// auto-detect anonymous authentication enablement here, but that would put extra burdon on the user to configure it +// correctly. In practice, its almost always one of the three configurations described above. +type anonAuthEnablement struct { + HealthCheckEndpointsAllowAnonAuth bool + OtherEndpointsAllowAnonAuth bool +} + +func isAnonymousAuthEnabled(config *rest.Config) (*anonAuthEnablement, error) { anonymousConfig := kubeclient.SecureAnonymousClientConfig(config) // we do not need either of these but RESTClientFor complains if they are not set @@ -380,32 +430,54 @@ func isAnonymousAuthEnabled(config *rest.Config) (bool, error) { // in case anyone looking at audit logs wants to know who is making the anonymous request anonymousConfig.UserAgent = rest.DefaultKubernetesUserAgent() - rc, err := rest.RESTClientFor(anonymousConfig) + anonClient, err := rest.RESTClientFor(anonymousConfig) if err != nil { - return false, err + return nil, err } + healthzAllowsAnonAuth, err := isAnonymousAuthEnabledForEndpoint(anonClient.Get().AbsPath("/healthz")) + if err != nil { + return nil, fmt.Errorf("error from healthz API: %w", err) + } + + // As a heuristic to determine if anonymous auth is enabled for all other API endpoints, + // we probe one representative endpoint which should exist on all clusters. We assume that + // the result found at this endpoint is representative of the result for all non-heath endpoints. + otherAPIAllowsAnonAuth, err := isAnonymousAuthEnabledForEndpoint( + anonClient.Get().AbsPath("/api/v1/nodes").Param("limit", "1"), + ) + if err != nil { + return nil, fmt.Errorf("error from v1 nodes API: %w", err) + } + + return &anonAuthEnablement{ + HealthCheckEndpointsAllowAnonAuth: healthzAllowsAnonAuth, + OtherEndpointsAllowAnonAuth: otherAPIAllowsAnonAuth, + }, nil +} + +func isAnonymousAuthEnabledForEndpoint(anonReq *rest.Request) (bool, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - _, errHealthz := rc.Get().AbsPath("/healthz").DoRaw(ctx) + _, err := anonReq.DoRaw(ctx) switch { - // 200 ok on healthz clearly indicates authentication success - case errHealthz == nil: + // 200 ok clearly indicates authentication success + case err == nil: return true, nil - // we are authenticated but not authorized. anonymous authentication is enabled - case apierrors.IsForbidden(errHealthz): + // we are authenticated but not authorized: anonymous authentication is enabled at the endpoint + case apierrors.IsForbidden(err): return true, nil // failure to authenticate will return unauthorized (http misnomer) - case apierrors.IsUnauthorized(errHealthz): + case apierrors.IsUnauthorized(err): return false, nil // any other error is unexpected default: - return false, errHealthz + return false, err } } diff --git a/internal/concierge/impersonator/impersonator_test.go b/internal/concierge/impersonator/impersonator_test.go index 09c3409f7..9e476069b 100644 --- a/internal/concierge/impersonator/impersonator_test.go +++ b/internal/concierge/impersonator/impersonator_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package impersonator @@ -101,19 +101,21 @@ func TestImpersonator(t *testing.T) { } tests := []struct { - name string - clientCert clientCert - clientImpersonateUser rest.ImpersonationConfig - clientMutateHeaders func(http.Header) - clientNextProtos []string - kubeAPIServerStatusCode int - kubeAPIServerHealthz http.Handler - anonymousAuthDisabled bool - noServiceAcctTokenInCache bool // when true, no available service account token for the impersonator to use - wantKubeAPIServerRequestHeaders func(credentialID string) http.Header - wantError string - wantConstructionError string - wantAuthorizerAttributes func(credentialID string) []authorizer.AttributesRecord + name string + clientCert clientCert + clientImpersonateUser rest.ImpersonationConfig + clientMutateHeaders func(http.Header) + clientNextProtos []string + kubeAPIServerStatusCode int + kubeAPIServerHealthz http.Handler + kubeAPIServerNodes http.Handler + anonymousAuthForHealthDisabled bool + anonymousAuthForOtherAPIsDisabled bool + noServiceAcctTokenInCache bool // when true, no available service account token for the impersonator to use + wantKubeAPIServerRequestHeaders func(credentialID string) http.Header + wantError string + wantConstructionError string + wantAuthorizerAttributes func(credentialID string) []authorizer.AttributesRecord }{ { name: "happy path", @@ -140,7 +142,7 @@ func TestImpersonator(t *testing.T) { }, }, { - name: "happy path with forbidden healthz", + name: "happy path with forbidden healthz (anonymous auth for health checks is allowed)", clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}), kubeAPIServerHealthz: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) @@ -168,13 +170,104 @@ func TestImpersonator(t *testing.T) { }, }, { - name: "happy path with unauthorized healthz", + name: "happy path with forbidden nodes (anonymous auth for other APIs is allowed)", + clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}), + kubeAPIServerNodes: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("no nodes for you")) + }), + wantKubeAPIServerRequestHeaders: func(credentialID string) http.Header { + return http.Header{ + "Impersonate-User": {"test-username"}, + "Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"}, + "Authorization": {"Bearer some-service-account-token"}, + "User-Agent": {"test-agent"}, + "Accept": {"application/vnd.kubernetes.protobuf,application/json"}, + "Accept-Encoding": {"gzip"}, + "X-Forwarded-For": {"127.0.0.1"}, + "Impersonate-Extra-Authentication.kubernetes.io%2fcredential-Id": {credentialID}, + } + }, + wantAuthorizerAttributes: func(credentialID string) []authorizer.AttributesRecord { + return []authorizer.AttributesRecord{ + { + User: defaultInfoForTestUsername(credentialID), + Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces", + }, + } + }, + }, + { + name: "happy path with unauthorized healthz (anonymous auth for health checks is disallowed)", clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}), kubeAPIServerHealthz: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte("no healthz for you")) }), - anonymousAuthDisabled: true, + anonymousAuthForHealthDisabled: true, + wantKubeAPIServerRequestHeaders: func(credentialID string) http.Header { + return http.Header{ + "Impersonate-User": {"test-username"}, + "Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"}, + "Authorization": {"Bearer some-service-account-token"}, + "User-Agent": {"test-agent"}, + "Accept": {"application/vnd.kubernetes.protobuf,application/json"}, + "Accept-Encoding": {"gzip"}, + "X-Forwarded-For": {"127.0.0.1"}, + "Impersonate-Extra-Authentication.kubernetes.io%2fcredential-Id": {credentialID}, + } + }, + wantAuthorizerAttributes: func(credentialID string) []authorizer.AttributesRecord { + return []authorizer.AttributesRecord{ + { + User: defaultInfoForTestUsername(credentialID), + Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces", + }, + } + }, + }, + { + name: "happy path with unauthorized nodes (anonymous auth for other APIs is disallowed)", + clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}), + kubeAPIServerNodes: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("no nodes for you")) + }), + anonymousAuthForOtherAPIsDisabled: true, + wantKubeAPIServerRequestHeaders: func(credentialID string) http.Header { + return http.Header{ + "Impersonate-User": {"test-username"}, + "Impersonate-Group": {"test-group1", "test-group2", "system:authenticated"}, + "Authorization": {"Bearer some-service-account-token"}, + "User-Agent": {"test-agent"}, + "Accept": {"application/vnd.kubernetes.protobuf,application/json"}, + "Accept-Encoding": {"gzip"}, + "X-Forwarded-For": {"127.0.0.1"}, + "Impersonate-Extra-Authentication.kubernetes.io%2fcredential-Id": {credentialID}, + } + }, + wantAuthorizerAttributes: func(credentialID string) []authorizer.AttributesRecord { + return []authorizer.AttributesRecord{ + { + User: defaultInfoForTestUsername(credentialID), + Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces", + }, + } + }, + }, + { + name: "happy path with unauthorized healthz and nodes (anonymous auth for everything is disallowed)", + clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}), + kubeAPIServerHealthz: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("no healthz for you")) + }), + kubeAPIServerNodes: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("no nodes for you")) + }), + anonymousAuthForHealthDisabled: true, + anonymousAuthForOtherAPIsDisabled: true, wantKubeAPIServerRequestHeaders: func(credentialID string) http.Header { return http.Header{ "Impersonate-User": {"test-username"}, @@ -747,7 +840,16 @@ func TestImpersonator(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("broken")) }), - wantConstructionError: `could not detect if anonymous authentication is enabled: an error on the server ("broken") has prevented the request from succeeding`, + wantConstructionError: `could not detect if anonymous authentication is enabled: error from healthz API: an error on the server ("broken") has prevented the request from succeeding`, + wantAuthorizerAttributes: nil, + }, + { + name: "unexpected nodes response", + kubeAPIServerNodes: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("broken")) + }), + wantConstructionError: `could not detect if anonymous authentication is enabled: error from v1 nodes API: an error on the server ("broken") has prevented the request from succeeding`, wantAuthorizerAttributes: nil, }, { @@ -832,7 +934,7 @@ func TestImpersonator(t *testing.T) { var testKubeAPIServerSawHeaders http.Header testKubeAPIServer, testKubeAPIServerCA := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsConfigFunc := func(rootCAs *x509.CertPool) *tls.Config { - // Requests to get configmaps, flowcontrol requests, and healthz requests + // Requests to get configmaps, nodes, flowcontrol requests, and healthz requests // are not done by our http round trippers that specify only one protocol // (either http1.1 or http2, not both). // For all other requests from the impersonator, if it is not an upgrade @@ -844,7 +946,8 @@ func TestImpersonator(t *testing.T) { case "/api/v1/namespaces/kube-system/configmaps", fmt.Sprintf("/apis/flowcontrol.apiserver.k8s.io/%s/prioritylevelconfigurations", priorityLevelConfigurationsVersion), fmt.Sprintf("/apis/flowcontrol.apiserver.k8s.io/%s/flowschemas", flowSchemasVersion), - "/healthz": + "/healthz", + "/api/v1/nodes": default: if !httpstream.IsUpgradeRequest(r) { secure.NextProtos = []string{secure.NextProtos[0]} @@ -900,6 +1003,15 @@ func TestImpersonator(t *testing.T) { _, _ = fmt.Fprint(w, "probed") return + case "/readyz", "/readyz/etcd", "/livez": + require.Equal(t, http.MethodGet, r.Method) + + // match the KAS endpoint's behavior + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + _, _ = fmt.Fprint(w, "ok") + return + case "/healthz": require.Equal(t, http.MethodGet, r.Method) require.Empty(t, r.Header.Get("Authorization")) @@ -916,6 +1028,24 @@ func TestImpersonator(t *testing.T) { _, _ = fmt.Fprint(w, "ok") return + case "/api/v1/nodes": + // In these tests, the test client doesn't call the nodes API through the impersonator, + // but the impersonator production code uses the nodes API to probe whether anonymous auth + // is enabled or not, similar to what it does with the healthz API. + require.Equal(t, http.MethodGet, r.Method) + require.Empty(t, r.Header.Get("Authorization")) + require.Contains(t, r.Header.Get("User-Agent"), "kubernetes") + + if tt.kubeAPIServerNodes != nil { + tt.kubeAPIServerNodes.ServeHTTP(w, r) + return + } + + // by default just return success + w.Header().Add("Content-Type", "application/json; charset=UTF-8") + _, _ = fmt.Fprint(w, `{}`) + return + case "/apis/login.concierge.pinniped.dev/v1alpha1/tokencredentialrequests": require.Equal(t, http.MethodPost, r.Method) @@ -1142,8 +1272,40 @@ func TestImpersonator(t *testing.T) { rc, err := rest.RESTClientFor(anonymousConfig) require.NoError(t, err) + // It would be nice to also call /healthz through the impersonator here, but this unit test makes it + // difficult because of the expectations that it sets differently for APIs that are expected to be called + // by external clients versus those expected to be called by the impersonator itself. + // We can test calling /readyz and /livez, but note that calling /healthz should also work the same. + readyzBody, errReadyz := rc.Get().AbsPath("/readyz").DoRaw(ctx) + if tt.anonymousAuthForHealthDisabled { + require.True(t, apierrors.IsUnauthorized(errReadyz), errReadyz) + require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(readyzBody)) + } else { + require.NoError(t, errReadyz) + require.Equal(t, "ok", string(readyzBody)) + } + + // We don't treat sub-paths of health check endpoints as health check endpoints. Treat sub-paths as "other" endpoints. + readyzEtcdBody, errReadyzEtcd := rc.Get().AbsPath("/readyz/etcd").DoRaw(ctx) + if tt.anonymousAuthForOtherAPIsDisabled { + require.True(t, apierrors.IsUnauthorized(errReadyzEtcd), errReadyzEtcd) + require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(readyzEtcdBody)) + } else { + require.NoError(t, errReadyzEtcd) + require.Equal(t, "ok", string(readyzEtcdBody)) + } + + livezBody, errLivez := rc.Get().AbsPath("/livez").DoRaw(ctx) + if tt.anonymousAuthForHealthDisabled { + require.True(t, apierrors.IsUnauthorized(errLivez), errLivez) + require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(livezBody)) + } else { + require.NoError(t, errLivez) + require.Equal(t, "ok", string(livezBody)) + } + probeBody, errProbe := rc.Get().AbsPath("/probe").DoRaw(ctx) - if tt.anonymousAuthDisabled { + if tt.anonymousAuthForOtherAPIsDisabled { require.True(t, apierrors.IsUnauthorized(errProbe), errProbe) require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(probeBody)) } else { @@ -1151,8 +1313,9 @@ func TestImpersonator(t *testing.T) { require.Equal(t, "probed", string(probeBody)) } + // Fetch other resource that just happens to also be called tokencredentialrequests, but belongs to a different API group/version. notTCRBody, errNotTCR := rc.Get().Resource("tokencredentialrequests").DoRaw(ctx) - if tt.anonymousAuthDisabled { + if tt.anonymousAuthForOtherAPIsDisabled { require.True(t, apierrors.IsUnauthorized(errNotTCR), errNotTCR) require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(notTCRBody)) } else { @@ -1161,7 +1324,7 @@ func TestImpersonator(t *testing.T) { } ducksBody, errDucks := rc.Get().Resource("ducks").DoRaw(ctx) - if tt.anonymousAuthDisabled { + if tt.anonymousAuthForOtherAPIsDisabled { require.True(t, apierrors.IsUnauthorized(errDucks), errDucks) require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(ducksBody)) } else { @@ -1194,8 +1357,24 @@ func TestImpersonator(t *testing.T) { Verb: "create", Namespace: "", APIGroup: "login.concierge.walrus.tld", APIVersion: "v1alpha1", Resource: "tokencredentialrequests", Subresource: "", Name: "", ResourceRequest: true, Path: "/apis/login.concierge.walrus.tld/v1alpha1/tokencredentialrequests", }, ) - if !tt.anonymousAuthDisabled { + if !tt.anonymousAuthForHealthDisabled { wantAuthorizerAttributes = append(wantAuthorizerAttributes, + authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil}, + Verb: "get", Namespace: "", APIGroup: "", APIVersion: "", Resource: "", Subresource: "", Name: "", ResourceRequest: false, Path: "/readyz", + }, + authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil}, + Verb: "get", Namespace: "", APIGroup: "", APIVersion: "", Resource: "", Subresource: "", Name: "", ResourceRequest: false, Path: "/livez", + }, + ) + } + if !tt.anonymousAuthForOtherAPIsDisabled { + wantAuthorizerAttributes = append(wantAuthorizerAttributes, + authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil}, + Verb: "get", Namespace: "", APIGroup: "", APIVersion: "", Resource: "", Subresource: "", Name: "", ResourceRequest: false, Path: "/readyz/etcd", + }, authorizer.AttributesRecord{ User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil}, Verb: "get", Namespace: "", APIGroup: "", APIVersion: "", Resource: "", Subresource: "", Name: "", ResourceRequest: false, Path: "/probe", @@ -2262,3 +2441,77 @@ func (r *attributeRecorder) record(attributes authorizer.Attributes) { defer r.lock.Unlock() r.attributes = append(r.attributes, *attributes.(*authorizer.AttributesRecord)) } + +func Test_isRequestForHealthCheck(t *testing.T) { + tests := []struct { + path string + want bool + }{ + { + path: "/healthz", + want: true, + }, + { + path: "/livez", + want: true, + }, + { + path: "/readyz", + want: true, + }, + { + path: "/healthz/anything", + want: false, + }, + { + path: "/livez/anything", + want: false, + }, + { + path: "/readyz/anything", + want: false, + }, + { + path: "/readyz/anything/", + want: false, + }, + { + path: "/healthz/", + want: false, + }, + { + path: "/livez/", + want: false, + }, + { + path: "/readyz/", + want: false, + }, + { + path: "/other", + want: false, + }, + { + path: "/something/else", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + u := url.URL{Path: tt.path} + got := isRequestForHealthCheck(&http.Request{URL: &u}) + require.Equal(t, tt.want, got) + }) + } + + t.Run("nil req", func(t *testing.T) { + got := isRequestForHealthCheck(nil) + require.False(t, got) + }) + + t.Run("nil URL", func(t *testing.T) { + got := isRequestForHealthCheck(&http.Request{}) + require.False(t, got) + }) +}