mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-02-06 13:01:18 +00:00
Merge pull request #2869 from vmware/rr/partial_anon_auth
support partial anonymous authentication in the impersonation proxy
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2021-2026 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
@@ -13,9 +13,13 @@ capabilities:
|
||||
# Will the cluster successfully provision a load balancer if requested?
|
||||
hasExternalLoadBalancerProvider: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthenticationSupported: false
|
||||
|
||||
# Are LDAP ports on the Internet reachable without interference from network firewalls or proxies?
|
||||
canReachInternetLDAPPorts: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthentication:
|
||||
healthEndpoints:
|
||||
allowed: false
|
||||
otherEndpoints:
|
||||
allowed: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2021-2026 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
@@ -13,9 +13,14 @@ capabilities:
|
||||
# Will the cluster successfully provision a load balancer if requested?
|
||||
hasExternalLoadBalancerProvider: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthenticationSupported: true
|
||||
|
||||
# Are LDAP ports on the Internet reachable without interference from network firewalls or proxies?
|
||||
canReachInternetLDAPPorts: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
# Amazon disabled anonymous authentication in EKS clusters starting with k8s 1.32.
|
||||
anonymousAuthentication:
|
||||
healthEndpoints:
|
||||
allowed: true
|
||||
otherEndpoints:
|
||||
allowedIfK8sMinorVersionLessThan: 32
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2020-2026 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
@@ -13,9 +13,14 @@ capabilities:
|
||||
# Will the cluster successfully provision a load balancer if requested?
|
||||
hasExternalLoadBalancerProvider: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthenticationSupported: true
|
||||
|
||||
# Are LDAP ports on the Internet reachable without interference from network firewalls or proxies?
|
||||
canReachInternetLDAPPorts: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
# Google disabled anonymous authentication in GKE clusters by default starting with k8s 1.35.
|
||||
anonymousAuthentication:
|
||||
healthEndpoints:
|
||||
allowed: true
|
||||
otherEndpoints:
|
||||
allowedIfK8sMinorVersionLessThan: 35
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2020-2026 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
@@ -13,9 +13,13 @@ capabilities:
|
||||
# Will the cluster successfully provision a load balancer if requested?
|
||||
hasExternalLoadBalancerProvider: false
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthenticationSupported: true
|
||||
|
||||
# Are LDAP ports on the Internet reachable without interference from network firewalls or proxies?
|
||||
canReachInternetLDAPPorts: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthentication:
|
||||
healthEndpoints:
|
||||
allowed: true
|
||||
otherEndpoints:
|
||||
allowed: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2020-2026 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
@@ -13,9 +13,13 @@ capabilities:
|
||||
# Will the cluster successfully provision a load balancer if requested?
|
||||
hasExternalLoadBalancerProvider: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthenticationSupported: true
|
||||
|
||||
# Are LDAP ports on the Internet reachable without interference from network firewalls or proxies?
|
||||
canReachInternetLDAPPorts: true
|
||||
|
||||
# Does the cluster allow requests without authentication?
|
||||
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests
|
||||
anonymousAuthentication:
|
||||
healthEndpoints:
|
||||
allowed: true
|
||||
otherEndpoints:
|
||||
allowed: true
|
||||
|
||||
@@ -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 integration
|
||||
@@ -218,7 +218,7 @@ func TestCredentialRequest_Browser(t *testing.T) {
|
||||
// for its localhost listener via --listen-port=env.CLIUpstreamOIDC.CallbackURL.Port() per oidcLoginCommand.
|
||||
// Since ports are global to the process, tests using oidcLoginCommand must be run serially.
|
||||
func TestCredentialRequest_JWTAuthenticatorRulesToDisallowLogin_Browser(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
|
||||
basicSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.CLIUpstreamOIDC.Issuer,
|
||||
@@ -321,7 +321,7 @@ func TestCredentialRequest_JWTAuthenticatorRulesToDisallowLogin_Browser(t *testi
|
||||
|
||||
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
|
||||
func TestCredentialRequest_ShouldFailWhenTheAuthenticatorDoesNotExist_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
@@ -344,7 +344,7 @@ func TestCredentialRequest_ShouldFailWhenTheAuthenticatorDoesNotExist_Parallel(t
|
||||
|
||||
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
|
||||
func TestCredentialRequest_ShouldFailWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
|
||||
// Create a testWebhook so we have a legitimate authenticator to pass to the TokenCredentialRequest API.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
@@ -365,7 +365,7 @@ func TestCredentialRequest_ShouldFailWhenTheRequestIsValidButTheTokenDoesNotAuth
|
||||
|
||||
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
|
||||
func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
|
||||
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
|
||||
// Create a testWebhook so we have a legitimate authenticator to pass to the TokenCredentialRequest API.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
|
||||
@@ -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 integration
|
||||
@@ -902,8 +902,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
whoAmI, err = impersonationProxyAnonymousPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
|
||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
|
||||
// we expect the impersonation proxy to match the behavior of KAS in regards to anonymous requests
|
||||
if env.HasCapability(testlib.AnonymousAuthenticationSupported) {
|
||||
// we expect the impersonation proxy to match the behavior of KAS in regard to anonymous requests
|
||||
if env.HasCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints) {
|
||||
require.NoError(t, err, testlib.Sdump(err))
|
||||
require.Equal(t,
|
||||
expectedWhoAmIRequestResponse(
|
||||
@@ -1409,8 +1409,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("anonymous authentication enabled", func(t *testing.T) {
|
||||
testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
|
||||
t.Run("anonymous authentication enabled for health checks", func(t *testing.T) {
|
||||
testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForHealthEndpoints)
|
||||
parallelIfNotEKS(t)
|
||||
|
||||
// anonymous auth enabled
|
||||
@@ -1421,11 +1421,50 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
t.Run("non-resource request", func(t *testing.T) {
|
||||
parallelIfNotEKS(t)
|
||||
|
||||
healthz, errHealth := impersonationProxyAnonymousRestClient.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||
require.NoError(t, errHealth, testlib.Sdump(errHealth))
|
||||
require.Equal(t, "ok", string(healthz))
|
||||
})
|
||||
response, err := impersonationProxyAnonymousRestClient.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||
require.NoError(t, err, testlib.Sdump(err))
|
||||
require.Equal(t, "ok", string(response))
|
||||
|
||||
response, err = impersonationProxyAnonymousRestClient.Get().AbsPath("/readyz").DoRaw(ctx)
|
||||
require.NoError(t, err, testlib.Sdump(err))
|
||||
require.Equal(t, "ok", string(response))
|
||||
|
||||
response, err = impersonationProxyAnonymousRestClient.Get().AbsPath("/livez").DoRaw(ctx)
|
||||
require.NoError(t, err, testlib.Sdump(err))
|
||||
require.Equal(t, "ok", string(response))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("anonymous authentication disabled for health checks", func(t *testing.T) {
|
||||
testlib.IntegrationEnv(t).WithoutCapability(testlib.AnonymousAuthenticationSupportedForHealthEndpoints)
|
||||
parallelIfNotEKS(t)
|
||||
|
||||
// anonymous auth disabled
|
||||
// - hit the healthz endpoint (non-resource endpoint)
|
||||
// - through the impersonation proxy
|
||||
// - should fail Unauthorized
|
||||
t.Run("non-resource request", func(t *testing.T) {
|
||||
parallelIfNotEKS(t)
|
||||
|
||||
expectedResponse := `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}` + "\n"
|
||||
|
||||
response, err := impersonationProxyAnonymousRestClient.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||
require.True(t, apierrors.IsUnauthorized(err), testlib.Sdump(err))
|
||||
require.Equal(t, expectedResponse, string(response))
|
||||
|
||||
response, err = impersonationProxyAnonymousRestClient.Get().AbsPath("/readyz").DoRaw(ctx)
|
||||
require.True(t, apierrors.IsUnauthorized(err), testlib.Sdump(err))
|
||||
require.Equal(t, expectedResponse, string(response))
|
||||
|
||||
response, err = impersonationProxyAnonymousRestClient.Get().AbsPath("/livez").DoRaw(ctx)
|
||||
require.True(t, apierrors.IsUnauthorized(err), testlib.Sdump(err))
|
||||
require.Equal(t, expectedResponse, string(response))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("anonymous authentication enabled for other endpoints", func(t *testing.T) {
|
||||
testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
parallelIfNotEKS(t)
|
||||
// - hit the pods endpoint (a resource endpoint)
|
||||
// - through the impersonation proxy
|
||||
// - should fail forbidden
|
||||
@@ -1462,22 +1501,10 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("anonymous authentication disabled", func(t *testing.T) {
|
||||
testlib.IntegrationEnv(t).WithoutCapability(testlib.AnonymousAuthenticationSupported)
|
||||
t.Run("anonymous authentication disabled for other endpoints", func(t *testing.T) {
|
||||
testlib.IntegrationEnv(t).WithoutCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
parallelIfNotEKS(t)
|
||||
|
||||
// - hit the healthz endpoint (non-resource endpoint)
|
||||
// - through the impersonation proxy
|
||||
// - should fail unauthorized
|
||||
// - kube api server should reject it
|
||||
t.Run("non-resource request", func(t *testing.T) {
|
||||
parallelIfNotEKS(t)
|
||||
|
||||
healthz, err := impersonationProxyAnonymousRestClient.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||
require.True(t, apierrors.IsUnauthorized(err), testlib.Sdump(err))
|
||||
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(healthz))
|
||||
})
|
||||
|
||||
// - hit the pods endpoint (a resource endpoint)
|
||||
// - through the impersonation proxy
|
||||
// - should fail unauthorized
|
||||
@@ -1491,7 +1518,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
require.Equal(t, &corev1.Pod{}, pod)
|
||||
})
|
||||
|
||||
// - request to whoami (pinniped resource endpoing)
|
||||
// - request to whoami (pinniped resource endpoint)
|
||||
// - through the impersonation proxy
|
||||
// - should fail unauthorized
|
||||
// - kube api server should reject it
|
||||
@@ -1982,7 +2009,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
// include an unsuccessful impersonation strategy saying that it was manually configured to be disabled.
|
||||
requireDisabledStrategy(ctx, t, env, adminConciergeClient)
|
||||
|
||||
if !env.HasCapability(testlib.ClusterSigningKeyIsAvailable) && env.HasCapability(testlib.AnonymousAuthenticationSupported) {
|
||||
if !env.HasCapability(testlib.ClusterSigningKeyIsAvailable) && env.HasCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints) {
|
||||
// This cluster does not support the cluster signing key strategy, so now that we've manually disabled the
|
||||
// impersonation strategy, we should be left with no working strategies.
|
||||
// Given that there are no working strategies, a TokenCredentialRequest which would otherwise work should now
|
||||
|
||||
@@ -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 integration
|
||||
|
||||
@@ -394,7 +394,7 @@ func maybeNeedsExtraWithSHA256(
|
||||
|
||||
// whoami requests are non-mutating and safe to run in parallel with serial tests, see main_test.go.
|
||||
func TestWhoAmI_Anonymous_Parallel(t *testing.T) {
|
||||
_ = testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
|
||||
_ = testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2022-2026 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
@@ -1040,11 +1040,11 @@ func TestOIDCClientSecretRequestUnauthenticated_Parallel(t *testing.T) {
|
||||
}, metav1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
|
||||
if env.KubernetesDistribution == testlib.AKSDistro {
|
||||
// On AKS the error just says "Unauthorized".
|
||||
require.Contains(t, err.Error(), "Unauthorized")
|
||||
} else {
|
||||
if env.HasCapability(testlib.AnonymousAuthenticationSupportedForOtherEndpoints) {
|
||||
// Clusters which allow anonymous auth will give a more detailed error.
|
||||
require.Contains(t, err.Error(), `User "system:anonymous" cannot create resource "oidcclientsecretrequests"`)
|
||||
} else {
|
||||
// On AKS and any other cluster which disallows anonymous auth, the error just says "Unauthorized".
|
||||
require.Contains(t, err.Error(), "Unauthorized")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 testlib
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -17,16 +18,18 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
type Capability string
|
||||
type KubeDistro string
|
||||
|
||||
const (
|
||||
ClusterSigningKeyIsAvailable Capability = "clusterSigningKeyIsAvailable"
|
||||
AnonymousAuthenticationSupported Capability = "anonymousAuthenticationSupported"
|
||||
HasExternalLoadBalancerProvider Capability = "hasExternalLoadBalancerProvider"
|
||||
CanReachInternetLDAPPorts Capability = "canReachInternetLDAPPorts"
|
||||
ClusterSigningKeyIsAvailable Capability = "clusterSigningKeyIsAvailable"
|
||||
HasExternalLoadBalancerProvider Capability = "hasExternalLoadBalancerProvider"
|
||||
CanReachInternetLDAPPorts Capability = "canReachInternetLDAPPorts"
|
||||
AnonymousAuthenticationSupportedForHealthEndpoints Capability = "anonymousAuthenticationSupportedForHealthEndpoints"
|
||||
AnonymousAuthenticationSupportedForOtherEndpoints Capability = "anonymousAuthenticationSupportedForOtherEndpoints"
|
||||
|
||||
KindDistro KubeDistro = "Kind"
|
||||
GKEDistro KubeDistro = "GKE"
|
||||
@@ -35,6 +38,24 @@ const (
|
||||
TKGSDistro KubeDistro = "TKGS"
|
||||
)
|
||||
|
||||
type Capabilities struct {
|
||||
ClusterSigningKeyIsAvailable bool `json:"clusterSigningKeyIsAvailable"`
|
||||
HasExternalLoadBalancerProvider bool `json:"hasExternalLoadBalancerProvider"`
|
||||
CanReachInternetLDAPPorts bool `json:"canReachInternetLDAPPorts"`
|
||||
|
||||
AnonymousAuthentication AnonymousAuthenticationCapabilities `json:"anonymousAuthentication"`
|
||||
}
|
||||
|
||||
type AnonymousAuthenticationCapabilities struct {
|
||||
HealthEndpoints AnonymousAuthenticationEndpointCapability `json:"healthEndpoints"`
|
||||
OtherEndpoints AnonymousAuthenticationEndpointCapability `json:"otherEndpoints"`
|
||||
}
|
||||
|
||||
type AnonymousAuthenticationEndpointCapability struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
AllowedIfK8sMinorVersionLessThan string `json:"allowedIfK8sMinorVersionLessThan"`
|
||||
}
|
||||
|
||||
// TestEnv captures all the external parameters consumed by our integration tests.
|
||||
type TestEnv struct {
|
||||
t *testing.T
|
||||
@@ -50,7 +71,7 @@ type TestEnv struct {
|
||||
SupervisorCustomLabels map[string]string `json:"supervisorCustomLabels"`
|
||||
ConciergeCustomLabels map[string]string `json:"conciergeCustomLabels"`
|
||||
KubernetesDistribution KubeDistro `json:"kubernetesDistribution"`
|
||||
Capabilities map[Capability]bool `json:"capabilities"`
|
||||
Capabilities Capabilities `json:"capabilities"`
|
||||
TestWebhook authenticationv1alpha1.WebhookAuthenticatorSpec `json:"testWebhook"`
|
||||
SupervisorHTTPSAddress string `json:"supervisorHttpsAddress"`
|
||||
SupervisorHTTPSIngressAddress string `json:"supervisorHttpsIngressAddress"`
|
||||
@@ -400,9 +421,34 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
||||
|
||||
func (e *TestEnv) HasCapability(cap Capability) bool {
|
||||
e.t.Helper()
|
||||
isCapable, capabilityWasDescribed := e.Capabilities[cap]
|
||||
require.Truef(e.t, capabilityWasDescribed, "the %q capability of the test environment was not described", cap)
|
||||
return isCapable
|
||||
|
||||
switch cap {
|
||||
case AnonymousAuthenticationSupportedForHealthEndpoints:
|
||||
versionLessThan := e.Capabilities.AnonymousAuthentication.HealthEndpoints.AllowedIfK8sMinorVersionLessThan
|
||||
if versionLessThan == "" {
|
||||
return e.Capabilities.AnonymousAuthentication.HealthEndpoints.Allowed
|
||||
}
|
||||
minorVersionNumber, err := strconv.Atoi(versionLessThan)
|
||||
require.NoError(e.t, err, "could not parse minor version number from AnonymousAuthentication HealthEndpoints capability as int")
|
||||
return !testutil.KubeServerMinorVersionAtLeastInclusive(e.t, NewKubernetesClientset(e.t).Discovery(), minorVersionNumber)
|
||||
case AnonymousAuthenticationSupportedForOtherEndpoints:
|
||||
versionLessThan := e.Capabilities.AnonymousAuthentication.OtherEndpoints.AllowedIfK8sMinorVersionLessThan
|
||||
if versionLessThan == "" {
|
||||
return e.Capabilities.AnonymousAuthentication.OtherEndpoints.Allowed
|
||||
}
|
||||
minorVersionNumber, err := strconv.Atoi(versionLessThan)
|
||||
require.NoError(e.t, err, "could not parse minor version number from AnonymousAuthentication OtherEndpoints capability as int")
|
||||
return !testutil.KubeServerMinorVersionAtLeastInclusive(e.t, NewKubernetesClientset(e.t).Discovery(), minorVersionNumber)
|
||||
case ClusterSigningKeyIsAvailable:
|
||||
return e.Capabilities.ClusterSigningKeyIsAvailable
|
||||
case HasExternalLoadBalancerProvider:
|
||||
return e.Capabilities.HasExternalLoadBalancerProvider
|
||||
case CanReachInternetLDAPPorts:
|
||||
return e.Capabilities.CanReachInternetLDAPPorts
|
||||
default:
|
||||
require.Failf(e.t, "unknown capability type", "name: %s", cap)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (e *TestEnv) WithCapability(cap Capability) *TestEnv {
|
||||
|
||||
Reference in New Issue
Block a user