impersonator: honor anonymous authentication being disabled
When anonymous authentication is disabled, the impersonation proxy will no longer authenticate anonymous requests other than calls to the token credential request API (this API is used to retrieve credentials and thus must be accessed anonymously). Signed-off-by: Benjamin A. Petersen <ben@benjaminapetersen.me> Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
parent
02335e2ade
commit
492f6cfddf
@ -19,6 +19,12 @@ also honor client certs from a CA that is specific to the impersonation proxy.
|
|||||||
This approach allows clients to use the Token Credential Request API even when
|
This approach allows clients to use the Token Credential Request API even when
|
||||||
we do not have the cluster's signing key.
|
we do not have the cluster's signing key.
|
||||||
|
|
||||||
|
The proxy will honor cluster configuration in regards to anonymous authentication.
|
||||||
|
When disabled, the proxy will not authenticate these requests. There is one caveat
|
||||||
|
in that Pinniped itself provides the Token Credential Request API which is used
|
||||||
|
specifically by anonymous users to retrieve credentials. This API is the single
|
||||||
|
API that will remain available even when anonymous authentication is disabled.
|
||||||
|
|
||||||
In terms of authorization, we rely mostly on the Kubernetes API server. Since we
|
In terms of authorization, we rely mostly on the Kubernetes API server. Since we
|
||||||
impersonate the user, the proxied request will be authorized against that user.
|
impersonate the user, the proxied request will be authorized against that user.
|
||||||
Thus for all regular REST verbs, we perform no authorization checks.
|
Thus for all regular REST verbs, we perform no authorization checks.
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
@ -198,9 +199,59 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
serverConfig.AuditPolicyChecker = policy.FakeChecker(auditinternal.LevelMetadata, nil)
|
serverConfig.AuditPolicyChecker = policy.FakeChecker(auditinternal.LevelMetadata, nil)
|
||||||
serverConfig.AuditBackend = &auditfake.Backend{}
|
serverConfig.AuditBackend = &auditfake.Backend{}
|
||||||
|
|
||||||
|
// Probe the API server to figure out if anonymous auth is enabled.
|
||||||
|
anonymousAuthEnabled, err := isAnonymousAuthEnabled(kubeClient.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)
|
||||||
|
|
||||||
// if we ever start unioning a TCR bearer token authenticator with serverConfig.Authenticator
|
// if we ever start unioning a TCR bearer token authenticator with serverConfig.Authenticator
|
||||||
// then we will need to update the related assumption in tokenPassthroughRoundTripper
|
// then we will need to update the related assumption in tokenPassthroughRoundTripper
|
||||||
|
|
||||||
|
delegatingAuthenticator := serverConfig.Authentication.Authenticator
|
||||||
|
blockAnonymousAuthenticator := &comparableAuthenticator{
|
||||||
|
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 {
|
||||||
|
return resp, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// authentication failed
|
||||||
|
if err != nil || !ok {
|
||||||
|
return resp, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// any other user than anonymous is irrelevant
|
||||||
|
if resp.User.GetName() != user.Anonymous {
|
||||||
|
return resp, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqInfo, ok := genericapirequest.RequestInfoFrom(req.Context())
|
||||||
|
if !ok {
|
||||||
|
return nil, false, constable.Error("no RequestInfo found in the context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// a TKR is a resource, any request that is not for a resource should not be authenticated
|
||||||
|
if !reqInfo.IsResourceRequest {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// any resource besides TKR should not be authenticated
|
||||||
|
if !isTokenCredReq(reqInfo) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// anonymous authentication is disabled, but we must let an anonymous request
|
||||||
|
// to TKR authenticate as this is the only method to retrieve credentials
|
||||||
|
return resp, ok, err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Set our custom authenticator before calling Compete(), which will use it.
|
||||||
|
serverConfig.Authentication.Authenticator = blockAnonymousAuthenticator
|
||||||
|
|
||||||
delegatingAuthorizer := serverConfig.Authorization.Authorizer
|
delegatingAuthorizer := serverConfig.Authorization.Authorizer
|
||||||
nestedImpersonationAuthorizer := &comparableAuthorizer{
|
nestedImpersonationAuthorizer := &comparableAuthorizer{
|
||||||
authorizerFunc: func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
authorizerFunc: func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
@ -230,16 +281,22 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
// Set our custom authorizer before calling Compete(), which will use it.
|
// Set our custom authorizer before calling Compete(), which will use it.
|
||||||
serverConfig.Authorization.Authorizer = nestedImpersonationAuthorizer
|
serverConfig.Authorization.Authorizer = nestedImpersonationAuthorizer
|
||||||
|
|
||||||
impersonationProxyServer, err := serverConfig.Complete().New("impersonation-proxy", genericapiserver.NewEmptyDelegate())
|
completedConfig := serverConfig.Complete()
|
||||||
|
impersonationProxyServer, err := completedConfig.New("impersonation-proxy", genericapiserver.NewEmptyDelegate())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
preparedRun := impersonationProxyServer.PrepareRun()
|
preparedRun := impersonationProxyServer.PrepareRun()
|
||||||
|
|
||||||
|
// Sanity check. Make sure that our custom authenticator is still in place and did not get changed or wrapped.
|
||||||
|
if completedConfig.Authentication.Authenticator != blockAnonymousAuthenticator {
|
||||||
|
return nil, fmt.Errorf("invalid mutation of anonymous authenticator detected: %#v", completedConfig.Authentication.Authenticator)
|
||||||
|
}
|
||||||
|
|
||||||
// Sanity check. Make sure that our custom authorizer is still in place and did not get changed or wrapped.
|
// Sanity check. Make sure that our custom authorizer is still in place and did not get changed or wrapped.
|
||||||
if preparedRun.Authorizer != nestedImpersonationAuthorizer {
|
if preparedRun.Authorizer != nestedImpersonationAuthorizer {
|
||||||
return nil, constable.Error("invalid mutation of impersonation authorizer detected")
|
return nil, fmt.Errorf("invalid mutation of impersonation authorizer detected: %#v", preparedRun.Authorizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check. Assert that we have a functioning token file to use and no bearer token.
|
// Sanity check. Assert that we have a functioning token file to use and no bearer token.
|
||||||
@ -262,6 +319,59 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAnonymousAuthEnabled(config *rest.Config) (bool, error) {
|
||||||
|
anonymousConfig := rest.AnonymousClientConfig(config)
|
||||||
|
|
||||||
|
// we do not need either of these but RESTClientFor complains if they are not set
|
||||||
|
anonymousConfig.GroupVersion = &schema.GroupVersion{}
|
||||||
|
anonymousConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, errHealthz := rc.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// 200 ok on healthz clearly indicates authentication success
|
||||||
|
case errHealthz == nil:
|
||||||
|
return true, nil
|
||||||
|
|
||||||
|
// we are authenticated but not authorized. anonymous authentication is enabled
|
||||||
|
case apierrors.IsForbidden(errHealthz):
|
||||||
|
return true, nil
|
||||||
|
|
||||||
|
// failure to authenticate will return unauthorized (http misnomer)
|
||||||
|
case apierrors.IsUnauthorized(errHealthz):
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
// any other error is unexpected
|
||||||
|
default:
|
||||||
|
return false, errHealthz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTokenCredReq(reqInfo *genericapirequest.RequestInfo) bool {
|
||||||
|
if reqInfo.Resource != "tokencredentialrequests" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// pinniped components allow for the group suffix to be customized
|
||||||
|
// rather than wiring in the current configured suffix, checking the prefix is sufficient
|
||||||
|
if !strings.HasPrefix(reqInfo.APIGroup, "login.concierge.") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func deleteKnownImpersonationHeaders(delegate http.Handler) http.Handler {
|
func deleteKnownImpersonationHeaders(delegate http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// remove known impersonation headers while avoiding mutation of input request
|
// remove known impersonation headers while avoiding mutation of input request
|
||||||
@ -290,6 +400,11 @@ func deleteKnownImpersonationHeaders(delegate http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No-op wrapping around RequestFunc to allow for comparisons.
|
||||||
|
type comparableAuthenticator struct {
|
||||||
|
authenticator.RequestFunc
|
||||||
|
}
|
||||||
|
|
||||||
// No-op wrapping around AuthorizerFunc to allow for comparisons.
|
// No-op wrapping around AuthorizerFunc to allow for comparisons.
|
||||||
type comparableAuthorizer struct {
|
type comparableAuthorizer struct {
|
||||||
authorizerFunc
|
authorizerFunc
|
||||||
|
@ -5,6 +5,7 @@ package impersonator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -17,8 +18,11 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
authenticationv1 "k8s.io/api/authentication/v1"
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||||
@ -33,10 +37,13 @@ import (
|
|||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
|
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/constable"
|
"go.pinniped.dev/internal/constable"
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
|
"go.pinniped.dev/internal/groupsuffix"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/httputil/roundtripper"
|
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||||
"go.pinniped.dev/internal/kubeclient"
|
"go.pinniped.dev/internal/kubeclient"
|
||||||
@ -72,6 +79,8 @@ func TestImpersonator(t *testing.T) {
|
|||||||
clientNextProtos []string
|
clientNextProtos []string
|
||||||
kubeAPIServerClientBearerTokenFile string
|
kubeAPIServerClientBearerTokenFile string
|
||||||
kubeAPIServerStatusCode int
|
kubeAPIServerStatusCode int
|
||||||
|
kubeAPIServerHealthz http.Handler
|
||||||
|
anonymousAuthDisabled bool
|
||||||
wantKubeAPIServerRequestHeaders http.Header
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
wantError string
|
wantError string
|
||||||
wantConstructionError string
|
wantConstructionError string
|
||||||
@ -90,6 +99,43 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with forbidden healthz",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
kubeAPIServerHealthz: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
_, _ = w.Write([]byte("no healthz for you"))
|
||||||
|
}),
|
||||||
|
wantKubeAPIServerRequestHeaders: 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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with unauthorized healthz",
|
||||||
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
kubeAPIServerHealthz: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, _ = w.Write([]byte("no healthz for you"))
|
||||||
|
}),
|
||||||
|
anonymousAuthDisabled: true,
|
||||||
|
wantKubeAPIServerRequestHeaders: 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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "happy path with upgrade",
|
name: "happy path with upgrade",
|
||||||
clientCert: newClientCert(t, ca, "test-username2", []string{"test-group3", "test-group4"}),
|
clientCert: newClientCert(t, ca, "test-username2", []string{"test-group3", "test-group4"}),
|
||||||
@ -333,6 +379,14 @@ func TestImpersonator(t *testing.T) {
|
|||||||
name: "no bearer token file in Kube API server client config",
|
name: "no bearer token file in Kube API server client config",
|
||||||
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unexpected healthz response",
|
||||||
|
kubeAPIServerHealthz: 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: an error on the server ("broken") has prevented the request from succeeding`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "header canonicalization user header",
|
name: "header canonicalization user header",
|
||||||
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
clientCert: newClientCert(t, ca, "test-username", []string{"test-group1", "test-group2"}),
|
||||||
@ -367,6 +421,9 @@ func TestImpersonator(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
// we need to create this listener ourselves because the API server
|
// we need to create this listener ourselves because the API server
|
||||||
// code treats (port == 0 && listener == nil) to mean "do nothing"
|
// code treats (port == 0 && listener == nil) to mean "do nothing"
|
||||||
listener, port, err := genericoptions.CreateListener("", "127.0.0.1:0", net.ListenConfig{})
|
listener, port, err := genericoptions.CreateListener("", "127.0.0.1:0", net.ListenConfig{})
|
||||||
@ -384,20 +441,26 @@ func TestImpersonator(t *testing.T) {
|
|||||||
testKubeAPIServerWasCalled := false
|
testKubeAPIServerWasCalled := false
|
||||||
var testKubeAPIServerSawHeaders http.Header
|
var testKubeAPIServerSawHeaders http.Header
|
||||||
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
testKubeAPIServerCA, testKubeAPIServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, http.MethodGet, r.Method)
|
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/v1/namespaces/kube-system/configmaps":
|
case "/api/v1/namespaces/kube-system/configmaps":
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
|
||||||
// The production code uses NewDynamicCAFromConfigMapController which fetches a ConfigMap,
|
// The production code uses NewDynamicCAFromConfigMapController which fetches a ConfigMap,
|
||||||
// so treat that differently. It wants to read the Kube API server CA from that ConfigMap
|
// so treat that differently. It wants to read the Kube API server CA from that ConfigMap
|
||||||
// to use it to validate client certs. We don't need it for this test, so return NotFound.
|
// to use it to validate client certs. We don't need it for this test, so return NotFound.
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "/api/v1/namespaces":
|
case "/api/v1/namespaces":
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
|
||||||
testKubeAPIServerWasCalled = true
|
testKubeAPIServerWasCalled = true
|
||||||
testKubeAPIServerSawHeaders = r.Header
|
testKubeAPIServerSawHeaders = r.Header
|
||||||
if tt.kubeAPIServerStatusCode != http.StatusOK {
|
if tt.kubeAPIServerStatusCode != http.StatusOK {
|
||||||
w.WriteHeader(tt.kubeAPIServerStatusCode)
|
w.WriteHeader(tt.kubeAPIServerStatusCode)
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
||||||
_, _ = w.Write([]byte(here.Doc(`
|
_, _ = w.Write([]byte(here.Doc(`
|
||||||
{
|
{
|
||||||
@ -409,9 +472,61 @@ func TestImpersonator(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
`)))
|
`)))
|
||||||
|
return
|
||||||
|
|
||||||
|
case "/probe":
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(w, "probed")
|
||||||
|
return
|
||||||
|
|
||||||
|
case "/healthz":
|
||||||
|
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.kubeAPIServerHealthz != nil {
|
||||||
|
tt.kubeAPIServerHealthz.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// by default just match the KAS /healthz endpoint
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
_, _ = fmt.Fprint(w, "ok")
|
||||||
|
return
|
||||||
|
|
||||||
|
case "/apis/login.concierge.pinniped.dev/v1alpha1/tokencredentialrequests":
|
||||||
|
require.Equal(t, http.MethodPost, r.Method)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
return
|
||||||
|
|
||||||
|
case "/apis/login.concierge.walrus.tld/v1alpha1/tokencredentialrequests":
|
||||||
|
require.Equal(t, http.MethodPost, r.Method)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
return
|
||||||
|
|
||||||
|
case "/apis/not-concierge.walrus.tld/v1/tokencredentialrequests":
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
_, _ = w.Write([]byte(`{"hello": "quack"}`))
|
||||||
|
return
|
||||||
|
|
||||||
|
case "/apis/not-concierge.walrus.tld/v1/ducks":
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
_, _ = w.Write([]byte(`{"hello": "birds"}`))
|
||||||
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
require.Fail(t, "fake Kube API server got an unexpected request")
|
require.Fail(t, "fake Kube API server got an unexpected request", "path: %s", r.URL.Path)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -485,7 +600,7 @@ func TestImpersonator(t *testing.T) {
|
|||||||
|
|
||||||
// The fake Kube API server knows how to to list namespaces, so make that request using the client
|
// The fake Kube API server knows how to to list namespaces, so make that request using the client
|
||||||
// through the impersonator.
|
// through the impersonator.
|
||||||
listResponse, err := client.Kubernetes.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
|
listResponse, err := client.Kubernetes.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
||||||
if len(tt.wantError) > 0 {
|
if len(tt.wantError) > 0 {
|
||||||
require.EqualError(t, err, tt.wantError)
|
require.EqualError(t, err, tt.wantError)
|
||||||
require.Equal(t, &corev1.NamespaceList{}, listResponse)
|
require.Equal(t, &corev1.NamespaceList{}, listResponse)
|
||||||
@ -505,6 +620,81 @@ func TestImpersonator(t *testing.T) {
|
|||||||
// of the original request mutated by the impersonator. Otherwise the headers should be nil.
|
// of the original request mutated by the impersonator. Otherwise the headers should be nil.
|
||||||
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
||||||
|
|
||||||
|
// anonymous TCR should always work
|
||||||
|
|
||||||
|
tcrRegGroup, err := kubeclient.New(kubeclient.WithConfig(rest.AnonymousClientConfig(clientKubeconfig)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tcrOtherGroup, err := kubeclient.New(kubeclient.WithConfig(rest.AnonymousClientConfig(clientKubeconfig)),
|
||||||
|
kubeclient.WithMiddleware(groupsuffix.New("walrus.tld")))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, errTCR := tcrRegGroup.PinnipedConcierge.LoginV1alpha1().TokenCredentialRequests().Create(ctx, &loginv1alpha1.TokenCredentialRequest{}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, errTCR)
|
||||||
|
|
||||||
|
_, errTCROtherGroup := tcrOtherGroup.PinnipedConcierge.LoginV1alpha1().TokenCredentialRequests().Create(ctx,
|
||||||
|
&loginv1alpha1.TokenCredentialRequest{
|
||||||
|
Spec: loginv1alpha1.TokenCredentialRequestSpec{
|
||||||
|
Authenticator: corev1.TypedLocalObjectReference{
|
||||||
|
APIGroup: pointer.String("anything.pinniped.dev"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, errTCROtherGroup)
|
||||||
|
|
||||||
|
// these calls should only work when anonymous auth is enabled
|
||||||
|
|
||||||
|
anonymousConfig := rest.AnonymousClientConfig(clientKubeconfig)
|
||||||
|
anonymousConfig.GroupVersion = &schema.GroupVersion{
|
||||||
|
Group: "not-concierge.walrus.tld",
|
||||||
|
Version: "v1",
|
||||||
|
}
|
||||||
|
anonymousConfig.APIPath = "/apis"
|
||||||
|
anonymousConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
|
||||||
|
rc, err := rest.RESTClientFor(anonymousConfig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
probeBody, errProbe := rc.Get().AbsPath("/probe").DoRaw(ctx)
|
||||||
|
if tt.anonymousAuthDisabled {
|
||||||
|
require.True(t, errors.IsUnauthorized(errProbe), errProbe)
|
||||||
|
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(probeBody))
|
||||||
|
} else {
|
||||||
|
require.NoError(t, errProbe)
|
||||||
|
require.Equal(t, "probed", string(probeBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
notTCRBody, errNotTCR := rc.Get().Resource("tokencredentialrequests").DoRaw(ctx)
|
||||||
|
if tt.anonymousAuthDisabled {
|
||||||
|
require.True(t, errors.IsUnauthorized(errNotTCR), errNotTCR)
|
||||||
|
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(notTCRBody))
|
||||||
|
} else {
|
||||||
|
require.NoError(t, errNotTCR)
|
||||||
|
require.Equal(t, `{"hello": "quack"}`, string(notTCRBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
ducksBody, errDucks := rc.Get().Resource("ducks").DoRaw(ctx)
|
||||||
|
if tt.anonymousAuthDisabled {
|
||||||
|
require.True(t, errors.IsUnauthorized(errDucks), errDucks)
|
||||||
|
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}`+"\n", string(ducksBody))
|
||||||
|
} else {
|
||||||
|
require.NoError(t, errDucks)
|
||||||
|
require.Equal(t, `{"hello": "birds"}`, string(ducksBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should always fail as unauthorized (even for TCR) because the cert is not valid
|
||||||
|
|
||||||
|
badCertConfig := rest.AnonymousClientConfig(clientKubeconfig)
|
||||||
|
badCert := newClientCert(t, unrelatedCA, "bad-user", []string{"bad-group"})
|
||||||
|
badCertConfig.TLSClientConfig.CertData = badCert.certPEM
|
||||||
|
badCertConfig.TLSClientConfig.KeyData = badCert.keyPEM
|
||||||
|
|
||||||
|
tcrBadCert, err := kubeclient.New(kubeclient.WithConfig(badCertConfig))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, errBadCert := tcrBadCert.PinnipedConcierge.LoginV1alpha1().TokenCredentialRequests().Create(ctx, &loginv1alpha1.TokenCredentialRequest{}, metav1.CreateOptions{})
|
||||||
|
require.True(t, errors.IsUnauthorized(errBadCert), errBadCert)
|
||||||
|
require.EqualError(t, errBadCert, "Unauthorized")
|
||||||
|
|
||||||
// Stop the impersonator server.
|
// Stop the impersonator server.
|
||||||
close(stopCh)
|
close(stopCh)
|
||||||
exitErr := <-errCh
|
exitErr := <-errCh
|
||||||
|
@ -39,12 +39,15 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/equality"
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
k8sinformers "k8s.io/client-go/informers"
|
k8sinformers "k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
@ -53,6 +56,7 @@ import (
|
|||||||
"k8s.io/client-go/util/certificate/csr"
|
"k8s.io/client-go/util/certificate/csr"
|
||||||
"k8s.io/client-go/util/keyutil"
|
"k8s.io/client-go/util/keyutil"
|
||||||
"k8s.io/client-go/util/retry"
|
"k8s.io/client-go/util/retry"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
conciergev1alpha "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
conciergev1alpha "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||||
@ -800,6 +804,9 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
).PinnipedConcierge
|
).PinnipedConcierge
|
||||||
whoAmI, err = impersonationProxyAnonymousPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
|
whoAmI, err = impersonationProxyAnonymousPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
|
||||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
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(library.AnonymousAuthenticationSupported) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t,
|
require.Equal(t,
|
||||||
expectedWhoAmIRequestResponse(
|
expectedWhoAmIRequestResponse(
|
||||||
@ -809,6 +816,9 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
),
|
),
|
||||||
whoAmI,
|
whoAmI,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
require.True(t, k8serrors.IsUnauthorized(err), library.Sdump(err))
|
||||||
|
}
|
||||||
|
|
||||||
// Test using a service account token.
|
// Test using a service account token.
|
||||||
namespaceName := createTestNamespace(t, adminClient)
|
namespaceName := createTestNamespace(t, adminClient)
|
||||||
@ -1193,6 +1203,173 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
actualConfigMap.TypeMeta = metav1.TypeMeta{} // This isn't filled out in the wantConfigMap we got back from create.
|
actualConfigMap.TypeMeta = metav1.TypeMeta{} // This isn't filled out in the wantConfigMap we got back from create.
|
||||||
require.Equal(t, *wantConfigMap, actualConfigMap)
|
require.Equal(t, *wantConfigMap, actualConfigMap)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("honors anonymous authentication of KAS", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
impersonationProxyAnonymousClient := newAnonymousImpersonationProxyClient(
|
||||||
|
t, impersonationProxyURL, impersonationProxyCACertPEM, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
copyConfig := rest.CopyConfig(impersonationProxyAnonymousClient.JSONConfig)
|
||||||
|
copyConfig.GroupVersion = &schema.GroupVersion{}
|
||||||
|
copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
|
||||||
|
impersonationProxyAnonymousRestClient, err := rest.RESTClientFor(copyConfig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
adminClientRestConfig := library.NewClientConfig(t)
|
||||||
|
clusterAdminCredentials := getCredForConfig(t, adminClientRestConfig)
|
||||||
|
impersonationProxyAdminClientAsAnonymousConfig := newImpersonationProxyClientWithCredentials(t,
|
||||||
|
clusterAdminCredentials,
|
||||||
|
impersonationProxyURL, impersonationProxyCACertPEM,
|
||||||
|
&rest.ImpersonationConfig{UserName: user.Anonymous}).
|
||||||
|
JSONConfig
|
||||||
|
impersonationProxyAdminClientAsAnonymousConfigCopy := rest.CopyConfig(impersonationProxyAdminClientAsAnonymousConfig)
|
||||||
|
impersonationProxyAdminClientAsAnonymousConfigCopy.GroupVersion = &schema.GroupVersion{}
|
||||||
|
impersonationProxyAdminClientAsAnonymousConfigCopy.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
|
||||||
|
impersonationProxyAdminRestClientAsAnonymous, err := rest.RESTClientFor(impersonationProxyAdminClientAsAnonymousConfigCopy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("anonymous authentication irrelevant", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// - hit the token credential request endpoint with an empty body
|
||||||
|
// - through the impersonation proxy
|
||||||
|
// - should succeed as an invalid request whether anonymous authentication is enabled or disabled
|
||||||
|
// - should not reject as unauthorized
|
||||||
|
t.Run("token credential request", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkr, err := impersonationProxyAnonymousClient.PinnipedConcierge.LoginV1alpha1().TokenCredentialRequests().
|
||||||
|
Create(ctx, &loginv1alpha1.TokenCredentialRequest{
|
||||||
|
Spec: loginv1alpha1.TokenCredentialRequestSpec{
|
||||||
|
Authenticator: corev1.TypedLocalObjectReference{APIGroup: pointer.String("anything.pinniped.dev")},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
require.True(t, k8serrors.IsInvalid(err), library.Sdump(err))
|
||||||
|
require.Equal(t, `.login.concierge.pinniped.dev "" is invalid: spec.token.value: Required value: token must be supplied`, err.Error())
|
||||||
|
require.Equal(t, &loginv1alpha1.TokenCredentialRequest{}, tkr)
|
||||||
|
})
|
||||||
|
|
||||||
|
// - hit the healthz endpoint (non-resource endpoint)
|
||||||
|
// - through the impersonation proxy
|
||||||
|
// - as cluster admin, impersonating anonymous user
|
||||||
|
// - should succeed, authentication happens as cluster-admin
|
||||||
|
// - whoami should confirm we are using impersonation
|
||||||
|
// - healthz should succeed, anonymous users can request this endpoint
|
||||||
|
// - healthz/log should fail, forbidden anonymous
|
||||||
|
t.Run("non-resource request while impersonating anonymous - nested impersonation", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
whoami, errWho := impersonationProxyAdminRestClientAsAnonymous.Post().Body([]byte(`{}`)).AbsPath("/apis/identity.concierge." + env.APIGroupSuffix + "/v1alpha1/whoamirequests").DoRaw(ctx)
|
||||||
|
require.NoError(t, errWho, library.Sdump(errWho))
|
||||||
|
require.True(t, strings.HasPrefix(string(whoami), `{"kind":"WhoAmIRequest","apiVersion":"identity.concierge.`+env.APIGroupSuffix+`/v1alpha1","metadata":{"creationTimestamp":null},"spec":{},"status":{"kubernetesUserInfo":{"user":{"username":"system:anonymous","groups":["system:unauthenticated"],"extra":{"original-user-info.impersonation-proxy.concierge.pinniped.dev":["{\"username\":`), string(whoami))
|
||||||
|
|
||||||
|
healthz, errHealth := impersonationProxyAdminRestClientAsAnonymous.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||||
|
require.NoError(t, errHealth, library.Sdump(errHealth))
|
||||||
|
require.Equal(t, "ok", string(healthz))
|
||||||
|
|
||||||
|
healthzLog, errHealthzLog := impersonationProxyAdminRestClientAsAnonymous.Get().AbsPath("/healthz/log").DoRaw(ctx)
|
||||||
|
require.True(t, k8serrors.IsForbidden(errHealthzLog), "%s\n%s", library.Sdump(errHealthzLog), string(healthzLog))
|
||||||
|
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User \"system:anonymous\" cannot get path \"/healthz/log\"","reason":"Forbidden","details":{},"code":403}`+"\n", string(healthzLog))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("anonymous authentication enabled", func(t *testing.T) {
|
||||||
|
library.IntegrationEnv(t).WithCapability(library.AnonymousAuthenticationSupported)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// anonymous auth enabled
|
||||||
|
// - hit the healthz endpoint (non-resource endpoint)
|
||||||
|
// - through the impersonation proxy
|
||||||
|
// - should succeed 200
|
||||||
|
// - should respond "ok"
|
||||||
|
t.Run("non-resource request", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
healthz, errHealth := impersonationProxyAnonymousRestClient.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||||
|
require.NoError(t, errHealth, library.Sdump(errHealth))
|
||||||
|
require.Equal(t, "ok", string(healthz))
|
||||||
|
})
|
||||||
|
|
||||||
|
// - hit the pods endpoint (a resource endpoint)
|
||||||
|
// - through the impersonation proxy
|
||||||
|
// - should fail forbidden
|
||||||
|
// - system:anonymous cannot get pods
|
||||||
|
t.Run("resource", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
pod, err := impersonationProxyAnonymousClient.Kubernetes.CoreV1().Pods(metav1.NamespaceSystem).
|
||||||
|
Get(ctx, "does-not-matter", metav1.GetOptions{})
|
||||||
|
require.True(t, k8serrors.IsForbidden(err), library.Sdump(err))
|
||||||
|
require.EqualError(t, err, `pods "does-not-matter" is forbidden: User "system:anonymous" cannot get resource "pods" in API group "" in the namespace "kube-system"`, library.Sdump(err))
|
||||||
|
require.Equal(t, &corev1.Pod{}, pod)
|
||||||
|
})
|
||||||
|
|
||||||
|
// - request to whoami (pinniped resource endpoing)
|
||||||
|
// - through the impersonation proxy
|
||||||
|
// - should succeed 200
|
||||||
|
// - should respond "you are system:anonymous"
|
||||||
|
t.Run("pinniped resource request", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
whoAmI, err := impersonationProxyAnonymousClient.PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||||
|
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t,
|
||||||
|
expectedWhoAmIRequestResponse(
|
||||||
|
"system:anonymous",
|
||||||
|
[]string{"system:unauthenticated"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
whoAmI,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("anonymous authentication disabled", func(t *testing.T) {
|
||||||
|
library.IntegrationEnv(t).WithoutCapability(library.AnonymousAuthenticationSupported)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// - 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) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
healthz, err := impersonationProxyAnonymousRestClient.Get().AbsPath("/healthz").DoRaw(ctx)
|
||||||
|
require.True(t, k8serrors.IsUnauthorized(err), library.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
|
||||||
|
// - kube api server should reject it
|
||||||
|
t.Run("resource", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
pod, err := impersonationProxyAnonymousClient.Kubernetes.CoreV1().Pods(metav1.NamespaceSystem).
|
||||||
|
Get(ctx, "does-not-matter", metav1.GetOptions{})
|
||||||
|
require.True(t, k8serrors.IsUnauthorized(err), library.Sdump(err))
|
||||||
|
require.Equal(t, &corev1.Pod{}, pod)
|
||||||
|
})
|
||||||
|
|
||||||
|
// - request to whoami (pinniped resource endpoing)
|
||||||
|
// - through the impersonation proxy
|
||||||
|
// - should fail unauthorized
|
||||||
|
// - kube api server should reject it
|
||||||
|
t.Run("pinniped resource request", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
whoAmI, err := impersonationProxyAnonymousClient.PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||||
|
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||||
|
require.True(t, k8serrors.IsUnauthorized(err), library.Sdump(err))
|
||||||
|
require.Equal(t, &identityv1alpha1.WhoAmIRequest{}, whoAmI)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("adding an annotation reconciles the LoadBalancer service", func(t *testing.T) {
|
t.Run("adding an annotation reconciles the LoadBalancer service", func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user