impersonator: encode proper API status on failure

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2021-03-13 20:25:23 -05:00
parent c82f568b2c
commit b530cef3b1
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
3 changed files with 64 additions and 17 deletions

View File

@ -12,14 +12,18 @@ import (
"strings" "strings"
"time" "time"
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/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/errors" "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/apiserver/pkg/server/filters" "k8s.io/apiserver/pkg/server/filters"
@ -227,7 +231,7 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
"url", r.URL.String(), "url", r.URL.String(),
"method", r.Method, "method", r.Method,
) )
http.Error(w, "invalid authorization header", http.StatusInternalServerError) newInternalErrResponse(w, r, c.Serializer, "invalid authorization header")
return return
} }
@ -237,7 +241,7 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
"url", r.URL.String(), "url", r.URL.String(),
"method", r.Method, "method", r.Method,
) )
http.Error(w, "invalid impersonation", http.StatusInternalServerError) newInternalErrResponse(w, r, c.Serializer, "invalid impersonation")
return return
} }
@ -247,7 +251,7 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
"url", r.URL.String(), "url", r.URL.String(),
"method", r.Method, "method", r.Method,
) )
http.Error(w, "invalid user", http.StatusInternalServerError) newInternalErrResponse(w, r, c.Serializer, "invalid user")
return return
} }
@ -257,7 +261,7 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
"url", r.URL.String(), "url", r.URL.String(),
"method", r.Method, "method", r.Method,
) )
http.Error(w, "unable to act as user", http.StatusUnprocessableEntity) newInternalErrResponse(w, r, c.Serializer, "unimplemented functionality - unable to act as current user")
return return
} }
@ -309,3 +313,18 @@ func getTransportForUser(userInfo user.Info, delegate http.RoundTripper) (http.R
// 8. the above would be safe even if in the future Kube started supporting UIDs asserted by client certs // 8. the above would be safe even if in the future Kube started supporting UIDs asserted by client certs
return nil, constable.Error("unexpected uid") return nil, constable.Error("unexpected uid")
} }
func newInternalErrResponse(w http.ResponseWriter, r *http.Request, s runtime.NegotiatedSerializer, msg string) {
newStatusErrResponse(w, r, s, apierrors.NewInternalError(constable.Error(msg)))
}
func newStatusErrResponse(w http.ResponseWriter, r *http.Request, s runtime.NegotiatedSerializer, err *apierrors.StatusError) {
requestInfo, ok := genericapirequest.RequestInfoFrom(r.Context())
if !ok {
responsewriters.InternalError(w, r, constable.Error("no RequestInfo found in the context"))
return
}
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
responsewriters.ErrorNegotiated(err, s, gv, w, r)
}

View File

@ -16,9 +16,12 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options" genericoptions "k8s.io/apiserver/pkg/server/options"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -284,6 +287,12 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
r, err := http.NewRequestWithContext(ctx, http.MethodGet, validURL.String(), nil) r, err := http.NewRequestWithContext(ctx, http.MethodGet, validURL.String(), nil)
require.NoError(t, err) require.NoError(t, err)
r.Header = h r.Header = h
reqInfo := &request.RequestInfo{
IsResourceRequest: false,
Path: validURL.Path,
Verb: "get",
}
r = r.WithContext(request.WithRequestInfo(ctx, reqInfo))
return r return r
} }
@ -324,44 +333,44 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
{ {
name: "Impersonate-User header already in request", name: "Impersonate-User header already in request",
request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}, nil), request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}, nil),
wantHTTPBody: "invalid impersonation\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusInternalServerError, wantHTTPStatus: http.StatusInternalServerError,
}, },
{ {
name: "Impersonate-Group header already in request", name: "Impersonate-Group header already in request",
request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}, nil), request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}, nil),
wantHTTPBody: "invalid impersonation\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusInternalServerError, wantHTTPStatus: http.StatusInternalServerError,
}, },
{ {
name: "Impersonate-Extra header already in request", name: "Impersonate-Extra header already in request",
request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}, nil), request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}, nil),
wantHTTPBody: "invalid impersonation\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusInternalServerError, wantHTTPStatus: http.StatusInternalServerError,
}, },
{ {
name: "Impersonate-* header already in request", name: "Impersonate-* header already in request",
request: newRequest(map[string][]string{"Impersonate-Something": {"some-newfangled-impersonate-header"}}, nil), request: newRequest(map[string][]string{"Impersonate-Something": {"some-newfangled-impersonate-header"}}, nil),
wantHTTPBody: "invalid impersonation\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusInternalServerError, wantHTTPStatus: http.StatusInternalServerError,
}, },
{ {
name: "unexpected authorization header", name: "unexpected authorization header",
request: newRequest(map[string][]string{"Authorization": {"panda"}}, nil), request: newRequest(map[string][]string{"Authorization": {"panda"}}, nil),
wantHTTPBody: "invalid authorization header\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid authorization header","reason":"InternalError","details":{"causes":[{"message":"invalid authorization header"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusInternalServerError, wantHTTPStatus: http.StatusInternalServerError,
}, },
{ {
name: "missing user", name: "missing user",
request: newRequest(map[string][]string{}, nil), request: newRequest(map[string][]string{}, nil),
wantHTTPBody: "invalid user\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid user","reason":"InternalError","details":{"causes":[{"message":"invalid user"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusInternalServerError, wantHTTPStatus: http.StatusInternalServerError,
}, },
{ {
name: "unexpected UID", name: "unexpected UID",
request: newRequest(map[string][]string{}, &user.DefaultInfo{UID: "007"}), request: newRequest(map[string][]string{}, &user.DefaultInfo{UID: "007"}),
wantHTTPBody: "unable to act as user\n", wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
wantHTTPStatus: http.StatusUnprocessableEntity, wantHTTPStatus: http.StatusInternalServerError,
}, },
// happy path // happy path
{ {
@ -458,9 +467,15 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, impersonatorHTTPHandlerFunc) require.NotNil(t, impersonatorHTTPHandlerFunc)
// this is not a valid way to get a server config, but it is good enough for a unit test
scheme := runtime.NewScheme()
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
codecs := serializer.NewCodecFactory(scheme)
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
w := httptest.NewRecorder() w := httptest.NewRecorder()
requestBeforeServe := tt.request.Clone(tt.request.Context()) requestBeforeServe := tt.request.Clone(tt.request.Context())
impersonatorHTTPHandlerFunc(nil).ServeHTTP(w, tt.request) impersonatorHTTPHandlerFunc(&serverConfig.Config).ServeHTTP(w, tt.request)
require.Equal(t, requestBeforeServe, tt.request, "ServeHTTP() mutated the request, and it should not per http.Handler docs") require.Equal(t, requestBeforeServe, tt.request, "ServeHTTP() mutated the request, and it should not per http.Handler docs")
if tt.wantHTTPStatus != 0 { if tt.wantHTTPStatus != 0 {

View File

@ -475,10 +475,23 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
impersonationProxyURL, impersonationProxyCACertPEM, "").PinnipedConcierge impersonationProxyURL, impersonationProxyCACertPEM, "").PinnipedConcierge
_, err = impersonationProxyServiceAccountPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests(). _, err = impersonationProxyServiceAccountPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{}) Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
require.Error(t, err) require.EqualError(t, err, "Internal error occurred: unimplemented functionality - unable to act as current user")
// The server checks that we have a UID in the request and rejects it with a 422 Unprocessable Entity. require.True(t, k8serrors.IsInternalError(err), err)
// The API machinery turns 422's into this error text... require.Equal(t, &k8serrors.StatusError{
require.Contains(t, err.Error(), "the server rejected our request due to an error in our request") ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusInternalServerError,
Reason: metav1.StatusReasonInternalError,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Message: "unimplemented functionality - unable to act as current user",
},
},
},
Message: "Internal error occurred: unimplemented functionality - unable to act as current user",
},
}, err)
}) })
t.Run("kubectl as a client", func(t *testing.T) { t.Run("kubectl as a client", func(t *testing.T) {